Skip to content

feat(interact): HITL no Cursor com AskQuestion e sky interact - #5

Merged
sraphaz merged 6 commits into
mainfrom
feat/sky-interact-hitl
Jul 29, 2026
Merged

feat(interact): HITL no Cursor com AskQuestion e sky interact#5
sraphaz merged 6 commits into
mainfrom
feat/sky-interact-hitl

Conversation

@sraphaz

@sraphaz sraphaz commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adiciona skill sky-interact, catálogo de decision points e CLI sky interact para pausar fluxos agenticos e elicitar escolhas (AskQuestion no Cursor, com fallback numerado).
  • Integra pending_interaction no journey.yaml após attach/assess/nova sessão, com auditoria human.interaction.*.
  • Atualiza sky-host, intake, coreografia v3.1 e docs (SKY_INTERACT, USER_JOURNEY, AGENTS).

Test plan

  • ./scripts/sky/sky.ps1 intake -Slug demo-interact
  • ./scripts/sky/sky.ps1 interact -Slug demo-interact -PointId arrival.intent — confere payload AskQuestion + pending_interaction no journey
  • ./scripts/sky/sky.ps1 interact -Slug demo-interact -Resolve -ChoiceId brownfield_repo — status resolved + evento de auditoria
  • Em Agent mode com modelo que expõe AskQuestion: abrir sessão e confirmar picker clicável nos pontos do catálogo
  • (Opcional) attach/assess em repo externo e verificar que o journey grava brownfield.after_attach / assess.next_action

Made with Cursor

Summary by CodeRabbit

  • New Features
    • Added HITL interaction decision points across arrival, assessment, attachment, optional elevation, export, and showcase flows.
    • Updated Cursor prompts to use AskQuestion first, with a numbered fallback when unavailable.
    • Introduced the interact command to request, resolve, or clear pending decisions, persisting choices, routing, and audit events in session journeys.
  • Documentation
    • Updated and added guides for the HITL interaction protocol, Cursor skills, CLI workflow, and interaction-turn schema validation.

Permite pausar fluxos agenticos (attach/assess/intake/gates) para elicitar escolhas na UI do Cursor, com fallback numerado e pending_interaction no journey.

Co-authored-by: Cursor <cursoragent@cursor.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@sraphaz, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c42f5c8-d60b-42ec-894a-870fdaa5d27b

📥 Commits

Reviewing files that changed from the base of the PR and between 617077b and b94b501.

📒 Files selected for processing (13)
  • .agents/interaction-points.yaml
  • docs/_meta/AGENT_AUTONOMY.md
  • docs/_meta/SKY_INTERACT.md
  • docs/schemas/interaction-turn.schema.yaml
  • scripts/agents/choreograph-agents.ps1
  • scripts/sky/assess-platform.ps1
  • scripts/sky/attach-plugin.ps1
  • scripts/sky/check-autonomy.ps1
  • scripts/sky/interaction-catalog.ps1
  • scripts/sky/new-session.ps1
  • scripts/sky/prompt-interaction.ps1
  • scripts/sky/sky.ps1
  • templates/sessions/example-horta/journey.yaml
📝 Walkthrough

Walkthrough

Adds a catalog-driven HITL workflow with interaction-point definitions, persisted pending_interaction state, PowerShell request/resolve/clear commands, AskQuestion-first Cursor guidance, fallback options, audit events, and orchestration updates across attach, assess, intake, delivery, and showcase flows.

Changes

HITL interaction workflow

Layer / File(s) Summary
Interaction catalog and state contracts
.agents/autonomy.yaml, .agents/interaction-points.yaml, docs/schemas/interaction-turn.schema.yaml, docs/_meta/SKY_INTERACT.md
Defines interaction lifecycle events, decision points, option routes, pending-interaction validation, and AskQuestion/fallback protocol rules.
Journey persistence and interaction CLI
scripts/sky/interaction-catalog.ps1, scripts/sky/prompt-interaction.ps1, scripts/sky/sky.ps1, scripts/sky/new-session.ps1, templates/sessions/example-horta/journey.yaml
Adds catalog lookup, journey YAML serialization, request/resolve/clear modes, CLI dispatch, and pending-interaction session templates.
Agent and phase orchestration
.agents/choreography.yaml, .agents/operational/*, scripts/agents/choreograph-agents.ps1, scripts/sky/assess-platform.ps1, scripts/sky/attach-plugin.ps1
Connects sky-interact to agents and post-assess/post-attach workflow triggers, including configured interaction hooks.
Cursor skill and workflow guidance
.cursor/rules/*, .cursor/skills/*, .skills/sky-interact.skill.yaml, AGENTS.md, docs/_meta/*
Documents and configures AskQuestion-first elicitation, fallback behavior, journey updates, privacy decisions, routing, and audit mappings.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant CursorAgent
  participant sky.ps1
  participant prompt-interaction.ps1
  participant journey.yaml
  User->>CursorAgent: Select an interaction option
  CursorAgent->>sky.ps1: Resolve choice
  sky.ps1->>prompt-interaction.ps1: Pass slug and choice id
  prompt-interaction.ps1->>journey.yaml: Persist resolved interaction
  journey.yaml-->>CursorAgent: Return route and next action
  CursorAgent->>User: Continue the selected workflow
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: Cursor HITL interactions via AskQuestion and the new sky interact flow.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sky-interact-hitl

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 7 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 7 issues found in the latest run.

  • ✅ Fixed: CLI catalog omits option commands
    • Adicionei os comandos faltantes ao catálogo embutido e preservei command no payload/opções gravados para que as escolhas continuem acionáveis.
  • ✅ Fixed: Resolve skips next suggested actions
    • O fluxo de resolve agora recalcula next_suggested_actions a partir da opção escolhida em vez de manter o menu anterior.
  • ✅ Fixed: Resolve overwrites pending created_at
    • O resolve agora reutiliza o created_at já persistido no pending_interaction e grava apenas um novo resolved_at.
  • ✅ Fixed: Resolve accepts invalid choice ids
    • O resolve passou a validar ChoiceId contra as opções disponíveis e falha explicitamente quando a escolha não existe.
  • ✅ Resolved by another fix: Resolve drops assess prompt override
    • Ao preservar o estado pendente original durante o resolve, o prompt customizado do assessment deixa de ser substituído pelo texto padrão do catálogo.
  • ✅ Fixed: Interact wipes suggested actions
    • As interações pendentes agora mesclam as ações sugeridas atuais com as já existentes em vez de sobrescrever todo o bloco.
  • ✅ Fixed: Attach leaves stale pending
    • O attach limpa o pending_interaction semeado por new-session antes do assess automático para evitar que arrival.intent fique stale.

Create PR

Or push these changes by commenting:

@cursor push c0ee531426
Preview (c0ee531426)
diff --git a/.agents/interaction-points.yaml b/.agents/interaction-points.yaml
--- a/.agents/interaction-points.yaml
+++ b/.agents/interaction-points.yaml
@@ -92,6 +92,7 @@
         routes_to: sky-elevator
       - id: status
         label: Só revisar o status
+        command: ./scripts/sky/sky.ps1 status -Slug {slug}
 
   - id: intake.deepen_gap
     phase: shape

diff --git a/scripts/sky/attach-plugin.ps1 b/scripts/sky/attach-plugin.ps1
--- a/scripts/sky/attach-plugin.ps1
+++ b/scripts/sky/attach-plugin.ps1
@@ -33,6 +33,7 @@
 
 $ErrorActionPreference = 'Stop'
 . (Join-Path $PSScriptRoot 'get-sky-config.ps1')
+. (Join-Path $PSScriptRoot 'interaction-catalog.ps1')
 
 $forgeRoot = Get-SkyRepoRoot
 $workspace = (Resolve-Path $WorkspacePath).Path
@@ -59,7 +60,9 @@
 }
 
 $sessionDir = Join-Path $forgeRoot ".sky\sessions\$Slug"
+$createdSession = $false
 if (-not (Test-Path $sessionDir)) {
+    $createdSession = $true
     Write-Host "Criando sessao brownfield: $Slug" -ForegroundColor Cyan
     & (Join-Path $PSScriptRoot 'new-session.ps1') -Slug $Slug
 }
@@ -74,6 +77,7 @@
 Set-Content -Path $profilePath -Value $profileYaml -Encoding UTF8
 
 $journeyPath = Join-Path $sessionDir 'journey.yaml'
+$runAssess = $Assess -or -not $NoAssess
 if (Test-Path $journeyPath) {
     $jRaw = Get-Content $journeyPath -Raw
     $now = (Get-Date).ToUniversalTime().ToString('o')
@@ -83,6 +87,9 @@
         $jRaw = $jRaw -replace '(?m)^slug:\s*.+$', "`$0`nhost_plugin: true`nprofile: $Profile"
     }
     Set-Content -Path $journeyPath -Value $jRaw -Encoding UTF8
+    if ($createdSession -and $runAssess) {
+        Clear-SkyJourneyPendingInteraction -JourneyPath $journeyPath
+    }
 }
 
 $pluginSrc = Join-Path $forgeRoot 'plugins\examples\sky-forge-host'
@@ -163,7 +170,6 @@
     }
 }
 
-$runAssess = $Assess -or -not $NoAssess
 if ($runAssess) {
     Write-Host ""
     & (Join-Path $PSScriptRoot 'assess-platform.ps1') -Slug $Slug -WorkspacePath $workspace

diff --git a/scripts/sky/interaction-catalog.ps1 b/scripts/sky/interaction-catalog.ps1
--- a/scripts/sky/interaction-catalog.ps1
+++ b/scripts/sky/interaction-catalog.ps1
@@ -27,9 +27,9 @@
         'brownfield.after_attach' = @{
             prompt = 'Host plugin anexado. Próximo passo?'
             options = @(
-                @{ id = 'run_assess'; label = 'Rodar assessment do repositório' }
+                @{ id = 'run_assess'; label = 'Rodar assessment do repositório'; command = './scripts/sky/sky.ps1 assess -Slug {slug} -WorkspacePath {workspace}' }
                 @{ id = 'deepen_problem'; label = 'Contar o problema de evolução (intake)'; routes_to = 'intake-conductor' }
-                @{ id = 'status'; label = 'Ver maturidade / status' }
+                @{ id = 'status'; label = 'Ver maturidade / status'; command = './scripts/sky/sky.ps1 status -Slug {slug}' }
                 @{ id = 'later'; label = 'Parar por aqui' }
             )
         }
@@ -37,9 +37,9 @@
             prompt = 'Assessment pronto. O que prefere agora?'
             options = @(
                 @{ id = 'deepen_top_gap'; label = 'Aprofundar a principal lacuna detectada'; routes_to = 'intake-conductor' }
-                @{ id = 'seed_roadmap'; label = 'Gerar roadmap de evolução (draft)' }
+                @{ id = 'seed_roadmap'; label = 'Gerar roadmap de evolução (draft)'; command = './scripts/sky/sky.ps1 seed-roadmap -Slug {slug}' }
                 @{ id = 'elevate'; label = 'Explorar elevação / índices SKY'; routes_to = 'sky-elevator' }
-                @{ id = 'status'; label = 'Só revisar o status' }
+                @{ id = 'status'; label = 'Só revisar o status'; command = './scripts/sky/sky.ps1 status -Slug {slug}' }
             )
         }
         'intake.deepen_gap' = @{
@@ -62,7 +62,7 @@
         'gate.approve_stage' = @{
             prompt = 'Há um gate humano pendente. Como seguir?'
             options = @(
-                @{ id = 'approve'; label = 'Aprovar este stage agora' }
+                @{ id = 'approve'; label = 'Aprovar este stage agora'; command = './scripts/sky/sky.ps1 approve -Slug {slug} -Stage {stage}' }
                 @{ id = 'explain'; label = 'Explicar o que o gate protege' }
                 @{ id = 'defer'; label = 'Deixar para depois' }
             )
@@ -70,8 +70,8 @@
         'deliver.export_scope' = @{
             prompt = 'Como quer o pacote de entrega?'
             options = @(
-                @{ id = 'partial'; label = 'Parcial (pronto para handoff cedo)' }
-                @{ id = 'full'; label = 'Completo (quando readiness permitir)' }
+                @{ id = 'partial'; label = 'Parcial (pronto para handoff cedo)'; command = './scripts/sky/sky.ps1 export -Slug {slug} -Completeness partial' }
+                @{ id = 'full'; label = 'Completo (quando readiness permitir)'; command = './scripts/sky/sky.ps1 export -Slug {slug} -Completeness full' }
                 @{ id = 'for_ai'; label = 'Pacote para IA (-ForAI)' }
                 @{ id = 'cancel'; label = 'Ainda não exportar' }
             )
@@ -149,6 +149,211 @@
     return $sb.ToString().TrimEnd()
 }
 
+function ConvertFrom-SkyYamlScalar {
+    param([AllowNull()][string]$Value)
+    if ($null -eq $Value) { return $null }
+    $trimmed = $Value.Trim()
+    if (-not $trimmed -or $trimmed -eq 'null') { return $null }
+    if ($trimmed.Length -ge 2 -and $trimmed.StartsWith('"') -and $trimmed.EndsWith('"')) {
+        return $trimmed.Substring(1, $trimmed.Length - 2)
+    }
+    return $trimmed
+}
+
+function Get-SkyJourneyPendingInteractionState {
+    param([Parameter(Mandatory = $true)][string]$JourneyPath)
+    if (-not (Test-Path $JourneyPath)) { return $null }
+
+    $lines = @(Get-Content $JourneyPath)
+    $i = 0
+    while ($i -lt $lines.Count) {
+        if ($lines[$i] -match '^pending_interaction\s*:') {
+            $state = [ordered]@{
+                id = $null
+                status = $null
+                channel = $null
+                created_at = $null
+                resolved_at = $null
+                choice_id = $null
+                choice_label = $null
+                prompt = $null
+                options = @()
+            }
+            $currentOption = $null
+            $i++
+            while ($i -lt $lines.Count) {
+                $line = $lines[$i]
+                if ($line -match '^[a-zA-Z_]') { break }
+                if ($line -match '^\s*$') {
+                    $i++
+                    continue
+                }
+                if ($line -match '^  options:\s*$') {
+                    $i++
+                    while ($i -lt $lines.Count) {
+                        $optionLine = $lines[$i]
+                        if ($optionLine -match '^[a-zA-Z_]') { break }
+                        if ($optionLine -match '^\s*$') {
+                            $i++
+                            continue
+                        }
+                        if ($optionLine -match '^    - id:\s*(.+)$') {
+                            if ($currentOption) {
+                                $state.options += [PSCustomObject]$currentOption
+                            }
+                            $currentOption = [ordered]@{ id = ConvertFrom-SkyYamlScalar $Matches[1] }
+                            $i++
+                            continue
+                        }
+                        if ($optionLine -match '^      label:\s*(.+)$') {
+                            $currentOption.label = ConvertFrom-SkyYamlScalar $Matches[1]
+                            $i++
+                            continue
+                        }
+                        if ($optionLine -match '^      routes_to:\s*(.+)$') {
+                            $currentOption.routes_to = ConvertFrom-SkyYamlScalar $Matches[1]
+                            $i++
+                            continue
+                        }
+                        if ($optionLine -match '^      command:\s*(.+)$') {
+                            $currentOption.command = ConvertFrom-SkyYamlScalar $Matches[1]
+                            $i++
+                            continue
+                        }
+                        if ($optionLine -notmatch '^    ') { break }
+                        $i++
+                    }
+                    if ($currentOption) {
+                        $state.options += [PSCustomObject]$currentOption
+                        $currentOption = $null
+                    }
+                    continue
+                }
+                if ($line -match '^  ([a-z_]+):\s*(.*)$') {
+                    $state[$Matches[1]] = ConvertFrom-SkyYamlScalar $Matches[2]
+                }
+                $i++
+            }
+            return [PSCustomObject]$state
+        }
+        $i++
+    }
+    return $null
+}
+
+function Get-SkyJourneyNextSuggestedActions {
+    param([Parameter(Mandatory = $true)][string]$JourneyPath)
+    if (-not (Test-Path $JourneyPath)) { return @() }
+
+    $lines = @(Get-Content $JourneyPath)
+    $actions = @()
+    $i = 0
+    while ($i -lt $lines.Count) {
+        if ($lines[$i] -match '^next_suggested_actions\s*:') {
+            $currentAction = $null
+            $i++
+            while ($i -lt $lines.Count) {
+                $line = $lines[$i]
+                if ($line -match '^[a-zA-Z_]') { break }
+                if ($line -match '^\s*$') {
+                    $i++
+                    continue
+                }
+                if ($line -match '^  - id:\s*(.+)$') {
+                    if ($currentAction) {
+                        $actions += [PSCustomObject]$currentAction
+                    }
+                    $currentAction = [ordered]@{ id = ConvertFrom-SkyYamlScalar $Matches[1] }
+                    $i++
+                    continue
+                }
+                if ($line -match '^    label:\s*(.+)$') {
+                    $currentAction.label = ConvertFrom-SkyYamlScalar $Matches[1]
+                    $i++
+                    continue
+                }
+                if ($line -match '^    agent:\s*(.+)$') {
+                    $currentAction.agent = ConvertFrom-SkyYamlScalar $Matches[1]
+                    $i++
+                    continue
+                }
+                if ($line -match '^    command:\s*(.+)$') {
+                    $currentAction.command = ConvertFrom-SkyYamlScalar $Matches[1]
+                    $i++
+                    continue
+                }
+                if ($line -notmatch '^    ') { break }
+                $i++
+            }
+            if ($currentAction) {
+                $actions += [PSCustomObject]$currentAction
+            }
+            break
+        }
+        $i++
+    }
+    return @($actions)
+}
+
+function ConvertTo-SkyNextSuggestedActions {
+    param([Parameter(Mandatory = $true)]$Options)
+
+    $actions = @()
+    foreach ($opt in @($Options)) {
+        if (-not $opt.id -or -not $opt.label) { continue }
+        $action = [ordered]@{
+            id = $opt.id
+            label = $opt.label
+        }
+        if ($opt.routes_to) { $action.agent = $opt.routes_to }
+        if ($opt.command) { $action.command = $opt.command }
+        $actions += [PSCustomObject]$action
+    }
+    return @($actions)
+}
+
+function Merge-SkyNextSuggestedActions {
+    param(
+        [Parameter()][array]$PrimaryActions = @(),
+        [Parameter()][array]$SecondaryActions = @()
+    )
+
+    $result = New-Object System.Collections.Generic.List[object]
+    $seen = @{}
+    foreach ($action in (@($PrimaryActions) + @($SecondaryActions))) {
+        if (-not $action -or -not $action.id -or -not $action.label) { continue }
+        $key = [string]$action.id
+        if ($seen.ContainsKey($key)) { continue }
+        $copy = [ordered]@{
+            id = $action.id
+            label = $action.label
+        }
+        if ($action.agent) { $copy.agent = $action.agent }
+        if ($action.command) { $copy.command = $action.command }
+        $result.Add([PSCustomObject]$copy)
+        $seen[$key] = $true
+    }
+    return $result.ToArray()
+}
+
+function ConvertTo-SkyNextActionsYamlLines {
+    param([Parameter(Mandatory = $true)]$Actions)
+
+    $lines = @()
+    foreach ($action in @($Actions)) {
+        if (-not $action.id -or -not $action.label) { continue }
+        $lines += "  - id: $($action.id)"
+        $safeLabel = $action.label -replace '"', ''''
+        $lines += "    label: `"$safeLabel`""
+        if ($action.agent) { $lines += "    agent: $($action.agent)" }
+        if ($action.command) {
+            $safeCommand = $action.command -replace '"', ''''
+            $lines += "    command: `"$safeCommand`""
+        }
+    }
+    return @($lines)
+}
+
 function Set-SkyJourneyPendingInteraction {
     param(
         [Parameter(Mandatory = $true)][string]$JourneyPath,

diff --git a/scripts/sky/prompt-interaction.ps1 b/scripts/sky/prompt-interaction.ps1
--- a/scripts/sky/prompt-interaction.ps1
+++ b/scripts/sky/prompt-interaction.ps1
@@ -61,29 +61,39 @@
 
 if ($Resolve) {
     if (-not $ChoiceId) { throw 'Resolve requer -ChoiceId' }
-    $raw = Get-Content $journeyPath -Raw
-    $existingId = 'unknown'
-    if ($raw -match '(?m)^pending_interaction:\s*\r?\n(?:  .*\r?\n)*?  id:\s*(\S+)') {
-        $existingId = $Matches[1]
-    }
-    $choiceLabel = $ChoiceId
+    $pending = Get-SkyJourneyPendingInteractionState -JourneyPath $journeyPath
+    $existingId = if ($pending -and $pending.id) { $pending.id } else { 'unknown' }
     $pointKey = if ($PointId) { $PointId } else { $existingId }
-    if ($catalog.ContainsKey($pointKey)) {
-        $match = $catalog[$pointKey].options | Where-Object { $_.id -eq $ChoiceId } | Select-Object -First 1
-        if ($match) { $choiceLabel = $match.label }
-        $Prompt = $catalog[$pointKey].prompt
-        $opts = $catalog[$pointKey].options
+    if ($pending -and $pending.options -and $pending.options.Count -gt 0) {
+        $opts = @($pending.options)
+    } elseif ($catalog.ContainsKey($pointKey)) {
+        $opts = @($catalog[$pointKey].options)
     } elseif ($OptionsJson) {
-        $opts = $OptionsJson | ConvertFrom-Json
+        $opts = @($OptionsJson | ConvertFrom-Json)
     } else {
-        $opts = @(@{ id = $ChoiceId; label = $ChoiceId })
+        throw 'Resolve requer opcoes conhecidas para validar -ChoiceId'
     }
-    if (-not $Prompt) { $Prompt = 'Decisao registrada.' }
+    $match = $opts | Where-Object { $_.id -eq $ChoiceId } | Select-Object -First 1
+    if (-not $match) {
+        throw "ChoiceId invalido para ${pointKey}: $ChoiceId"
+    }
+    $choiceLabel = $match.label
+    if (-not $Prompt) {
+        if ($pending -and $pending.prompt) {
+            $Prompt = $pending.prompt
+        } elseif ($catalog.ContainsKey($pointKey)) {
+            $Prompt = $catalog[$pointKey].prompt
+        } else {
+            $Prompt = 'Decisao registrada.'
+        }
+    }
     $now = (Get-Date).ToUniversalTime().ToString('o')
+    $createdAt = if ($pending -and $pending.created_at) { $pending.created_at } else { $now }
     $block = Format-SkyPendingInteractionYaml -PointId $pointKey -Prompt $Prompt -Options $opts `
         -Status 'resolved' -Channel $Channel -ChoiceId $ChoiceId -ChoiceLabel $choiceLabel `
-        -CreatedAt $now -ResolvedAt $now
-    Set-SkyJourneyPendingInteraction -JourneyPath $journeyPath -PendingYamlBlock $block
+        -CreatedAt $createdAt -ResolvedAt $now
+    $nextLines = ConvertTo-SkyNextActionsYamlLines -Actions (ConvertTo-SkyNextSuggestedActions -Options @($match))
+    Set-SkyJourneyPendingInteraction -JourneyPath $journeyPath -PendingYamlBlock $block -NextActionsYamlLines $nextLines
     if (-not $NoAudit -and (Test-Path $rec)) {
         & $rec -Slug $Slug -AgentId 'sky-host' -Action 'human.interaction.answered' -Outcome 'ok' `
             -AutonomyLevel 'route' -Details "point=$pointKey choice=$ChoiceId" -ErrorAction SilentlyContinue | Out-Null
@@ -125,14 +135,11 @@
 $block = Format-SkyPendingInteractionYaml -PointId $(if ($PointId) { $PointId } else { 'custom' }) `
     -Prompt $Prompt -Options $opts -Status 'pending' -Channel $Channel -CreatedAt $now
 
-$nextLines = @()
-$i = 0
-foreach ($opt in $opts) {
-    $i++
-    $nextLines += "  - id: $($opt.id)"
-    $nextLines += "    label: `"$($opt.label -replace '"', '''')`""
-    if ($opt.routes_to) { $nextLines += "    agent: $($opt.routes_to)" }
-}
+$nextLines = ConvertTo-SkyNextActionsYamlLines -Actions (
+    Merge-SkyNextSuggestedActions `
+        -PrimaryActions (ConvertTo-SkyNextSuggestedActions -Options $opts) `
+        -SecondaryActions (Get-SkyJourneyNextSuggestedActions -JourneyPath $journeyPath)
+)
 
 Set-SkyJourneyPendingInteraction -JourneyPath $journeyPath -PendingYamlBlock $block -NextActionsYamlLines $nextLines
 
@@ -150,7 +157,10 @@
     point_id = $PointId
     prompt = $Prompt
     options = @($opts | ForEach-Object {
-            [ordered]@{ id = $_.id; label = $_.label }
+            $optionPayload = [ordered]@{ id = $_.id; label = $_.label }
+            if ($_.routes_to) { $optionPayload.routes_to = $_.routes_to }
+            if ($_.command) { $optionPayload.command = $_.command }
+            $optionPayload
         })
     fallback = [ordered]@{
         format = 'numbered_chat'

You can send follow-ups to the cloud agent here.

Comment thread scripts/sky/interaction-catalog.ps1
Comment thread scripts/sky/prompt-interaction.ps1 Outdated
Comment thread scripts/sky/prompt-interaction.ps1 Outdated
Comment thread scripts/sky/prompt-interaction.ps1 Outdated
Comment thread scripts/sky/prompt-interaction.ps1 Outdated
Comment thread scripts/sky/prompt-interaction.ps1 Outdated
Comment thread scripts/sky/attach-plugin.ps1

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f6689bb1e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .agents/choreography.yaml
Comment on lines +24 to +27
- id: post_assess
description: Após sky assess — elicitar assess.next_action
- id: post_attach
description: Após sky attach — elicitar brownfield.after_attach (se sem assess)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept the newly declared choreography triggers

Add post_assess and post_attach to the trigger values accepted by scripts/agents/choreograph-agents.ps1:13; otherwise any caller attempting to resolve either newly declared trigger is rejected during PowerShell parameter binding, so the intake-sessions rule can never be activated through those trigger names.

Useful? React with 👍 / 👎.

Comment thread scripts/sky/interaction-catalog.ps1 Outdated
Comment on lines +39 to +42
@{ id = 'deepen_top_gap'; label = 'Aprofundar a principal lacuna detectada'; routes_to = 'intake-conductor' }
@{ id = 'seed_roadmap'; label = 'Gerar roadmap de evolução (draft)' }
@{ id = 'elevate'; label = 'Explorar elevação / índices SKY'; routes_to = 'sky-elevator' }
@{ id = 'status'; label = 'Só revisar o status' }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve action metadata in the embedded catalog

Include the command and other action metadata from .agents/interaction-points.yaml in this embedded mirror (or read the source catalog directly). For example, invoking assess.next_action persists seed_roadmap without its defined command, and the generated next_suggested_actions and pending block therefore contain no executable route for that choice; the same loss affects attach, gate, export, elevation-policy, and privacy options. This undermines the sky-host's repository-defined routing role.

AGENTS.md reference: AGENTS.md:L38-L42

Useful? React with 👍 / 👎.

Comment thread scripts/sky/prompt-interaction.ps1 Outdated
Comment on lines +71 to +73
if ($catalog.ContainsKey($pointKey)) {
$match = $catalog[$pointKey].options | Where-Object { $_.id -eq $ChoiceId } | Select-Object -First 1
if ($match) { $choiceLabel = $match.label }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject choices that are not offered by the pending interaction

Require $match before marking a catalog interaction resolved. If the agent or user mistypes a choice ID, this branch currently records human.interaction.answered successfully and writes the unknown ID as the selected label even though no catalog route or command exists, leaving the session falsely resolved and impossible to route from the reported answer.

Useful? React with 👍 / 👎.

Comment thread scripts/sky/prompt-interaction.ps1 Outdated
Comment on lines +83 to +86
$block = Format-SkyPendingInteractionYaml -PointId $pointKey -Prompt $Prompt -Options $opts `
-Status 'resolved' -Channel $Channel -ChoiceId $ChoiceId -ChoiceLabel $choiceLabel `
-CreatedAt $now -ResolvedAt $now
Set-SkyJourneyPendingInteraction -JourneyPath $journeyPath -PendingYamlBlock $block

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Replace stale next actions when resolving a choice

Pass updated NextActionsYamlLines when resolving, rather than only replacing pending_interaction. A normal request first rewrites next_suggested_actions to every offered option, so after -Resolve this call leaves all rejected alternatives advertised as next steps; when the session is resumed, consumers can suggest or execute a path the user explicitly did not choose.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (2)
scripts/sky/new-session.ps1 (1)

176-200: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

arrival.intent prompt/options are duplicated instead of sourced from the catalog.

This heredoc hardcodes the same prompt/options already defined in Get-SkyInteractionCatalog (scripts/sky/interaction-catalog.ps1). Any future edit to the arrival.intent entry must be applied in three places (catalog.ps1, .agents/interaction-points.yaml, and here) or sessions will start with a stale prompt.

♻️ Proposed direction: build the block from the catalog
+. (Join-Path $PSScriptRoot 'interaction-catalog.ps1')
+
 if ($f -eq 'journey.yaml') {
+    $arrival = (Get-SkyInteractionCatalog())['arrival.intent']
+    $pendingBlock = Format-SkyPendingInteractionYaml -PointId 'arrival.intent' -Prompt $arrival.prompt -Options $arrival.options -CreatedAt $now
     $content = @"
 ...
-pending_interaction:
-  id: arrival.intent
-  ...
+$pendingBlock
 "@
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/sky/new-session.ps1` around lines 176 - 200, Update the
session-generation logic around the pending_interaction block to source the
arrival.intent prompt and options from Get-SkyInteractionCatalog instead of
hardcoding them in the heredoc. Preserve the existing generated fields and
routing behavior while ensuring catalog changes are reflected automatically in
new sessions.
scripts/sky/interaction-catalog.ps1 (1)

8-96: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Mirror drift risk for the interaction catalog.

Get-SkyInteractionCatalog is documented as a manual copy of .agents/interaction-points.yaml, but it is still an independent hashtable. Keep a single source of truth or generate this file from the YAML catalog to avoid options/prompts diverging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/sky/interaction-catalog.ps1` around lines 8 - 96, Update
Get-SkyInteractionCatalog so it no longer maintains an independent manual
catalog; use .agents/interaction-points.yaml as the single source of truth or
generate the PowerShell hashtable from that YAML. Ensure all prompts, option
IDs, labels, and routes remain synchronized with the YAML catalog.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.agents/interaction-points.yaml:
- Around line 130-143: Update the gate.approve_stage interaction point’s
human-gate stages to align with the declared human_gates configuration: retain
architecture and add elevation alongside brief and package, ensuring both stages
can reach the existing approve command and approval flow.

In @.agents/operational/sky-host.agent.yaml:
- Around line 41-44: Update the arrival_prompts configuration used by the
sky-host to comply with the one-question HITL contract: replace the three
simultaneous prompts with the catalog prompt/reference, or retain exactly one
context-appropriate question selected through arrival.intent. Ensure the
sky-host presents only one decision per turn.

In @.cursor/rules/sky-host.mdc:
- Around line 32-36: The interaction-resolution guidance permits state updates
without the required audit record and selected-choice details. In
.cursor/rules/sky-host.mdc lines 32-36, remove the manual pending_interaction
shortcut or require the complete resolved state, selected choice, next action,
and human.interaction.answered audit fields; in .skills/sky-interact.skill.yaml
lines 14-17, delegate resolution to the audited resolver and enforce exactly one
follow-up action.
- Around line 18-21: Use one complete session-state contract at every response
entrypoint: update .cursor/rules/sky-host.mdc (lines 18-21),
.cursor/skills/sky-experience/SKILL.md (lines 21-25),
.cursor/skills/sky-interact/SKILL.md (lines 30-33), and
.skills/sky-interact.skill.yaml (lines 9-13) to read maturity.yaml,
sky-merits.yaml, brief-draft.yaml, alternatives.yaml, ux-spec.yaml, and
journey.yaml before responding or eliciting, including the top gaps for
elevation and ux_design; preserve the existing pending-interaction and no-slug
behavior.
- Line 65: The interaction catalog is inconsistent between host routing and
journey documentation. In .cursor/rules/sky-host.mdc at line 65, route through
brownfield.after_attach before assess.next_action or remove that decision point
from the documented flow; in docs/_meta/USER_JOURNEY.md at lines 67-73, add
journey rows for elevate.confirm, gate.approve_stage, deliver.export_scope, and
implement.agentic_repo.

In @.cursor/rules/sky-intake.mdc:
- Around line 22-26: Atualize o fluxo de lacunas em sky-intake.mdc para sempre
persistir pending_interaction em journey.yaml usando a interação documentada
intake.deepen_gap antes de fazer a pergunta ao usuário. Remova o caráter
opcional dessa etapa e mantenha a atualização dos artefatos da dimensão
escolhida somente após a resposta ser resolvida.

In `@docs/schemas/interaction-turn.schema.yaml`:
- Around line 14-16: Update the interaction schema’s status-dependent validation
so records with status resolved must include resolved_at and at least one
non-null answer: choice_id or freeform. Preserve the existing validation for
pending and cleared statuses, and ensure incomplete resolved interactions are
rejected.

In `@scripts/sky/assess-platform.ps1`:
- Around line 195-207: The post-action interaction flow must not silently skip
creation or suppress failures. In scripts/sky/assess-platform.ps1 lines 195-207,
update the prompt-interaction.ps1 handling to fail with an actionable error when
unavailable or when invocation fails, or use the supported CLI interaction
command directly; preserve the existing assess.next_action prompt. Apply the
same failure handling to scripts/sky/attach-plugin.ps1 lines 170-176 so
successful attach and assessment operations always create the documented
next-step interaction.

In `@scripts/sky/interaction-catalog.ps1`:
- Around line 98-150: Update Format-SkyPendingInteractionYaml to YAML-escape all
text inserted into double-quoted scalars, including Prompt, ChoiceLabel, option
labels, and any later nextLines labels derived from options. Escape backslashes
and normalize embedded newlines, while preserving valid YAML output for existing
text and keeping the current null handling unchanged.

In `@scripts/sky/prompt-interaction.ps1`:
- Around line 62-86: Update the Resolve branch around the existingId extraction
to also parse and retain the existing pending_interaction created_at value from
$raw. Pass that preserved timestamp to Format-SkyPendingInteractionYaml via
-CreatedAt, while continuing to use $now for -ResolvedAt; provide an appropriate
fallback only when the original timestamp is unavailable.

---

Nitpick comments:
In `@scripts/sky/interaction-catalog.ps1`:
- Around line 8-96: Update Get-SkyInteractionCatalog so it no longer maintains
an independent manual catalog; use .agents/interaction-points.yaml as the single
source of truth or generate the PowerShell hashtable from that YAML. Ensure all
prompts, option IDs, labels, and routes remain synchronized with the YAML
catalog.

In `@scripts/sky/new-session.ps1`:
- Around line 176-200: Update the session-generation logic around the
pending_interaction block to source the arrival.intent prompt and options from
Get-SkyInteractionCatalog instead of hardcoding them in the heredoc. Preserve
the existing generated fields and routing behavior while ensuring catalog
changes are reflected automatically in new sessions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 74f0f193-198b-40f5-a6eb-fbf1a296401c

📥 Commits

Reviewing files that changed from the base of the PR and between a9bab1c and 1f6689b.

📒 Files selected for processing (24)
  • .agents/autonomy.yaml
  • .agents/choreography.yaml
  • .agents/interaction-points.yaml
  • .agents/operational/intake-conductor.agent.yaml
  • .agents/operational/sky-host.agent.yaml
  • .cursor/rules/sky-core.mdc
  • .cursor/rules/sky-host.mdc
  • .cursor/rules/sky-intake.mdc
  • .cursor/skills/sky-experience/SKILL.md
  • .cursor/skills/sky-interact/SKILL.md
  • .skills/sky-interact.skill.yaml
  • AGENTS.md
  • docs/_meta/AGENT_AUTONOMY.md
  • docs/_meta/AGENT_OBSERVABILITY.md
  • docs/_meta/SKY_INTERACT.md
  • docs/_meta/USER_JOURNEY.md
  • docs/schemas/interaction-turn.schema.yaml
  • scripts/sky/assess-platform.ps1
  • scripts/sky/attach-plugin.ps1
  • scripts/sky/interaction-catalog.ps1
  • scripts/sky/new-session.ps1
  • scripts/sky/prompt-interaction.ps1
  • scripts/sky/sky.ps1
  • templates/sessions/example-horta/journey.yaml

Comment thread .agents/interaction-points.yaml
Comment thread .agents/operational/sky-host.agent.yaml Outdated
Comment thread .cursor/rules/sky-host.mdc Outdated
Comment thread .cursor/rules/sky-host.mdc Outdated
Comment thread .cursor/rules/sky-host.mdc Outdated
Comment thread .cursor/rules/sky-intake.mdc Outdated
Comment thread docs/schemas/interaction-turn.schema.yaml
Comment thread scripts/sky/assess-platform.ps1 Outdated
Comment thread scripts/sky/interaction-catalog.ps1
Comment thread scripts/sky/prompt-interaction.ps1 Outdated
Preserve journey hints and pending timestamps, validate ChoiceId, emit option commands, and clear stale arrival pending on attach.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread scripts/sky/interaction-catalog.ps1
Comment thread scripts/sky/interaction-catalog.ps1
Comment thread scripts/sky/prompt-interaction.ps1
Comment thread scripts/sky/prompt-interaction.ps1
Load interaction catalog from YAML, harden resolve/audit contracts, accept post_assess/post_attach triggers, and fail closed when post-action HITL cannot be written.

Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

There are 7 total unresolved issues (including 3 from previous reviews).

Autofix Details

Bugbot Autofix prepared fixes for all 4 issues found in the latest run.

  • ✅ Fixed: Command placeholders never substituted
    • A resolucao agora preserva contexto de workspace/stage no pending e substitui placeholders em next_suggested_actions e no payload resolvido.
  • ✅ Fixed: Attach CLI blocks no-assess HITL
    • sky.ps1 attach agora encaminha -Assess e -NoAssess para attach-plugin.ps1, liberando o fluxo HITL sem assessment.
  • ✅ Fixed: Pending cleared before attach finishes
    • O attach deixou de limpar pending_interaction antes de concluir o link e só o sobrescreve quando o proximo passo bem-sucedido e gravado.
  • ✅ Fixed: Resolve ignores pending options
    • A resolucao agora valida e reutiliza primeiro as opcoes persistidas em journey.yaml, preservando exatamente o que foi perguntado ao usuario.

Create PR

Or push these changes by commenting:

@cursor push 3e56042e6c
Preview (3e56042e6c)
diff --git a/scripts/sky/attach-plugin.ps1 b/scripts/sky/attach-plugin.ps1
--- a/scripts/sky/attach-plugin.ps1
+++ b/scripts/sky/attach-plugin.ps1
@@ -83,10 +83,6 @@
         $jRaw = $jRaw -replace '(?m)^slug:\s*.+$', "`$0`nhost_plugin: true`nprofile: $Profile"
     }
     Set-Content -Path $journeyPath -Value $jRaw -Encoding UTF8
-
-    # Limpar arrival.intent (ou outro pending) herdado de new-session — evita elicitar chegada em fase shape
-    . (Join-Path $PSScriptRoot 'interaction-catalog.ps1')
-    Clear-SkyJourneyPendingInteraction -JourneyPath $journeyPath
 }
 
 $pluginSrc = Join-Path $forgeRoot 'plugins\examples\sky-forge-host'
@@ -178,7 +174,7 @@
     }
     Write-Host ""
     Write-Host "=== Proximo passo (interacao HITL) ===" -ForegroundColor Cyan
-    & $interactScript -Slug $Slug -PointId 'brownfield.after_attach'
+    & $interactScript -Slug $Slug -PointId 'brownfield.after_attach' -WorkspacePath $workspace
 }
 
 Write-Host ""

diff --git a/scripts/sky/interaction-catalog.ps1 b/scripts/sky/interaction-catalog.ps1
--- a/scripts/sky/interaction-catalog.ps1
+++ b/scripts/sky/interaction-catalog.ps1
@@ -16,6 +16,27 @@
     return $t
 }
 
+function Unescape-SkyYamlDoubleQuoted {
+    param([AllowNull()][string]$Text)
+    if ($null -eq $Text) { return $null }
+    $t = $Text.Trim()
+    if (-not $t -or $t -eq 'null') { return $null }
+    if (
+        $t.Length -ge 2 -and (
+            ($t.StartsWith('"') -and $t.EndsWith('"')) -or
+            ($t.StartsWith("'") -and $t.EndsWith("'"))
+        )
+    ) {
+        $t = $t.Substring(1, $t.Length - 2)
+    }
+    $escapedSlash = [char]0x1F
+    $t = $t.Replace('\\', [string]$escapedSlash)
+    $t = $t.Replace('\"', '"')
+    $t = $t.Replace('\n', "`n")
+    $t = $t.Replace([string]$escapedSlash, '\')
+    return $t
+}
+
 function ConvertFrom-SkyInteractionPointsYaml {
     param([Parameter(Mandatory = $true)][string]$Path)
     if (-not (Test-Path $Path)) { return $null }
@@ -148,8 +169,15 @@
         status     = $null
         prompt     = $null
         created_at = $null
+        resolved_at = $null
         channel    = $null
+        choice_id = $null
+        choice_label = $null
+        workspace = $null
+        stage = $null
         option_ids = @()
+        options    = @()
+        metadata   = @{}
     }
 
     if ($JourneyRaw -notmatch '(?m)^pending_interaction\s*:') { return $snap }
@@ -160,28 +188,109 @@
     }
     if (-not $pendingChunk) { return $snap }
 
-    if ($pendingChunk -match '(?m)^  id:\s*(\S+)') { $snap.id = $Matches[1].Trim() }
-    if ($pendingChunk -match '(?m)^  status:\s*(\S+)') { $snap.status = $Matches[1].Trim() }
-    if ($pendingChunk -match '(?m)^  channel:\s*(\S+)') { $snap.channel = $Matches[1].Trim() }
-    if ($pendingChunk -match '(?m)^  created_at:\s*"([^"]+)"') {
-        $snap.created_at = $Matches[1]
-    } elseif ($pendingChunk -match '(?m)^  created_at:\s*(\S+)') {
-        $snap.created_at = $Matches[1].Trim().Trim('"')
+    $lines = @($pendingChunk -split '\r?\n')
+    $inOptions = $false
+    $currentOption = $null
+
+    foreach ($line in $lines) {
+        if ($inOptions) {
+            if ($line -match '^    - id:\s*(\S+)') {
+                if ($null -ne $currentOption) {
+                    if (-not $currentOption.Contains('label')) { $currentOption.label = $currentOption.id }
+                    $snap.options += [PSCustomObject]$currentOption
+                }
+                $currentOption = [ordered]@{ id = $Matches[1].Trim() }
+                continue
+            }
+            if ($null -ne $currentOption) {
+                if ($line -match '^      label:\s*(.+)$') {
+                    $currentOption.label = Unescape-SkyYamlDoubleQuoted $Matches[1]
+                    continue
+                }
+                if ($line -match '^      routes_to:\s*(\S+)') {
+                    $currentOption.routes_to = $Matches[1].Trim()
+                    continue
+                }
+                if ($line -match '^      command:\s*(.+)$') {
+                    $currentOption.command = Unescape-SkyYamlDoubleQuoted $Matches[1]
+                    continue
+                }
+            }
+            if ($line -match '^\s*$') { continue }
+            if ($line -notmatch '^    ' -and $line -notmatch '^      ') {
+                if ($null -ne $currentOption) {
+                    if (-not $currentOption.Contains('label')) { $currentOption.label = $currentOption.id }
+                    $snap.options += [PSCustomObject]$currentOption
+                    $currentOption = $null
+                }
+                $inOptions = $false
+            }
+        }
+
+        if ($line -match '^  options:\s*$') {
+            $inOptions = $true
+            continue
+        }
+        if ($line -match '^  ([a-z_][a-z0-9_]*):\s*(.*)$') {
+            $key = $Matches[1]
+            $value = Unescape-SkyYamlDoubleQuoted $Matches[2]
+            switch ($key) {
+                'id' { $snap.id = $value }
+                'status' { $snap.status = $value }
+                'channel' { $snap.channel = $value }
+                'created_at' { $snap.created_at = $value }
+                'resolved_at' { $snap.resolved_at = $value }
+                'choice_id' { $snap.choice_id = $value }
+                'choice_label' { $snap.choice_label = $value }
+                'prompt' { $snap.prompt = $value }
+                'workspace' {
+                    $snap.workspace = $value
+                    $snap.metadata[$key] = $value
+                }
+                'stage' {
+                    $snap.stage = $value
+                    $snap.metadata[$key] = $value
+                }
+                default {
+                    $snap.metadata[$key] = $value
+                }
+            }
+        }
     }
-    if ($pendingChunk -match '(?m)^  prompt:\s*"((?:\\.|[^"])*)"') {
-        $snap.prompt = $Matches[1] -replace "''", "'"
-    } elseif ($pendingChunk -match '(?m)^  prompt:\s*(.+)$') {
-        $snap.prompt = $Matches[1].Trim().Trim('"')
+
+    if ($null -ne $currentOption) {
+        if (-not $currentOption.Contains('label')) { $currentOption.label = $currentOption.id }
+        $snap.options += [PSCustomObject]$currentOption
     }
-    $ids = [regex]::Matches($pendingChunk, '(?m)^    - id:\s*(\S+)')
-    $snap.option_ids = @($ids | ForEach-Object { $_.Groups[1].Value })
+    $snap.option_ids = @($snap.options | ForEach-Object { $_.id })
     return $snap
 }
 
+function Resolve-SkyInteractionTemplate {
+    param(
+        [AllowNull()][string]$Template,
+        [hashtable]$Values = @{}
+    )
+    if ([string]::IsNullOrEmpty($Template)) { return $Template }
+    $resolved = $Template
+    foreach ($key in @($Values.Keys | Sort-Object)) {
+        $value = $Values[$key]
+        if ($null -eq $value -or [string]::IsNullOrWhiteSpace([string]$value)) { continue }
+        $pattern = '\{' + [regex]::Escape([string]$key) + '\}'
+        $replacement = [string]$value
+        $resolved = [regex]::Replace($resolved, $pattern, [System.Text.RegularExpressions.MatchEvaluator]{
+                param($m)
+                return $replacement
+            })
+    }
+    return $resolved
+}
+
 function Format-SkyOptionNextActionLines {
     param(
         [Parameter(Mandatory = $true)]$Option,
-        [string]$Slug = ''
+        [string]$Slug = '',
+        [hashtable]$Context = @{}
     )
     $lines = @()
     $lines += "  - id: $($Option.id)"
@@ -189,8 +298,14 @@
     $lines += "    label: `"$lbl`""
     if ($Option.routes_to) { $lines += "    agent: $($Option.routes_to)" }
     if ($Option.command) {
-        $cmd = $Option.command
-        if ($Slug) { $cmd = $cmd -replace '\{slug\}', $Slug }
+        $cmdContext = @{}
+        if ($Context) {
+            foreach ($key in $Context.Keys) {
+                $cmdContext[$key] = $Context[$key]
+            }
+        }
+        if ($Slug) { $cmdContext.slug = $Slug }
+        $cmd = Resolve-SkyInteractionTemplate -Template $Option.command -Values $cmdContext
         $lines += "    command: `"$(Escape-SkyYamlDoubleQuoted $cmd)`""
     }
     return $lines
@@ -201,6 +316,7 @@
         [Parameter(Mandatory = $true)][string]$PointId,
         [Parameter(Mandatory = $true)][string]$Prompt,
         [Parameter(Mandatory = $true)]$Options,
+        [hashtable]$Metadata = @{},
         [string]$Status = 'pending',
         [string]$Channel = 'ask_question',
         [string]$ChoiceId = '',
@@ -235,6 +351,12 @@
     }
     $promptSafe = Escape-SkyYamlDoubleQuoted $Prompt
     [void]$sb.AppendLine("  prompt: `"$promptSafe`"")
+    foreach ($key in @($Metadata.Keys | Sort-Object)) {
+        $value = $Metadata[$key]
+        if ($null -eq $value -or [string]::IsNullOrWhiteSpace([string]$value)) { continue }
+        $safe = Escape-SkyYamlDoubleQuoted ([string]$value)
+        [void]$sb.AppendLine("  $key: `"$safe`"")
+    }
     [void]$sb.AppendLine('  options:')
     foreach ($opt in $Options) {
         [void]$sb.AppendLine("    - id: $($opt.id)")

diff --git a/scripts/sky/prompt-interaction.ps1 b/scripts/sky/prompt-interaction.ps1
--- a/scripts/sky/prompt-interaction.ps1
+++ b/scripts/sky/prompt-interaction.ps1
@@ -30,6 +30,12 @@
     [string]$ChoiceId,
 
     [Parameter()]
+    [string]$WorkspacePath,
+
+    [Parameter()]
+    [string]$Stage,
+
+    [Parameter()]
     [string]$Channel = 'ask_question',
 
     [Parameter()]
@@ -59,6 +65,79 @@
 
 $catalog = Get-SkyInteractionCatalog
 
+function Resolve-SkyInteractionWorkspacePath {
+    param([string]$Path)
+    if (-not $Path) { return $null }
+    try {
+        return (Resolve-Path $Path -ErrorAction Stop).Path
+    } catch {
+        return $Path
+    }
+}
+
+function Get-SkyInteractionPendingMetadata {
+    param(
+        [string]$Slug,
+        [string]$WorkspacePath,
+        [string]$Stage,
+        $Options
+    )
+    $metadata = @{}
+    if ($Stage) {
+        $metadata.stage = $Stage
+    }
+    $needsWorkspace = $WorkspacePath -or @($Options | Where-Object {
+            $_.command -and $_.command -match '\{workspace\}'
+        }).Count -gt 0
+    if ($WorkspacePath) {
+        $metadata.workspace = Resolve-SkyInteractionWorkspacePath -Path $WorkspacePath
+    } elseif ($needsWorkspace -and $Slug) {
+        $resolveLink = Join-Path $PSScriptRoot 'resolve-sky-link.ps1'
+        if (Test-Path $resolveLink) {
+            . $resolveLink
+            try {
+                $ctx = Get-SkyLinkContext -Slug $Slug
+                if ($ctx -and $ctx.WorkspacePath) {
+                    $metadata.workspace = $ctx.WorkspacePath
+                }
+            } catch {
+                # Sem link ainda: manter placeholder se nao houver workspace conhecido.
+            }
+        }
+    }
+    return $metadata
+}
+
+function Get-SkyInteractionResolveContext {
+    param(
+        [string]$Slug,
+        $PendingSnapshot,
+        [string]$WorkspacePath,
+        [string]$Stage,
+        $Options
+    )
+    $context = @{}
+    if ($PendingSnapshot -and $PendingSnapshot.metadata) {
+        foreach ($key in $PendingSnapshot.metadata.Keys) {
+            $value = $PendingSnapshot.metadata[$key]
+            if ($null -ne $value -and -not [string]::IsNullOrWhiteSpace([string]$value)) {
+                $context[$key] = $value
+            }
+        }
+    }
+    $runtimeMetadata = Get-SkyInteractionPendingMetadata -Slug $Slug -WorkspacePath $WorkspacePath -Stage $Stage -Options $Options
+    foreach ($key in $runtimeMetadata.Keys) {
+        $value = $runtimeMetadata[$key]
+        if ($null -ne $value -and -not [string]::IsNullOrWhiteSpace([string]$value)) {
+            $context[$key] = $value
+        }
+    }
+    if ($Slug) {
+        $context.slug = $Slug
+    }
+    return $context
+}
+
 if ($Resolve) {
     if (-not $ChoiceId) { throw 'Resolve requer -ChoiceId' }
     $raw = Get-Content $journeyPath -Raw
@@ -68,6 +147,8 @@
     $opts = @()
     if ($OptionsJson) {
         $opts = @($OptionsJson | ConvertFrom-Json)
+    } elseif ($snap.options.Count -gt 0) {
+        $opts = @($snap.options)
     } elseif ($catalog.ContainsKey($pointKey)) {
         $opts = @($catalog[$pointKey].options)
     }
@@ -93,17 +174,26 @@
     $createdAt = if ($snap.created_at) { $snap.created_at } else { (Get-Date).ToUniversalTime().ToString('o') }
     $resolvedChannel = if ($snap.channel) { $snap.channel } else { $Channel }
     $now = (Get-Date).ToUniversalTime().ToString('o')
-
-    # Opcoes no bloco resolved: preferir catalogo (com command) se o ponto existir
-    if ($catalog.ContainsKey($pointKey)) {
-        $opts = @($catalog[$pointKey].options)
+    $context = Get-SkyInteractionResolveContext -Slug $Slug -PendingSnapshot $snap `
+        -WorkspacePath $WorkspacePath -Stage $Stage -Options $opts
+    $resolvedMetadata = @{}
+    if ($snap.metadata) {
+        foreach ($key in $snap.metadata.Keys) {
+            $resolvedMetadata[$key] = $snap.metadata[$key]
+        }
     }
+    foreach ($key in $context.Keys) {
+        if ($key -ne 'slug') {
+            $resolvedMetadata[$key] = $context[$key]
+        }
+    }
 
     $block = Format-SkyPendingInteractionYaml -PointId $pointKey -Prompt $resolvedPrompt -Options $opts `
+        -Metadata $resolvedMetadata `
         -Status 'resolved' -Channel $resolvedChannel -ChoiceId $ChoiceId -ChoiceLabel $choiceLabel `
         -CreatedAt $createdAt -ResolvedAt $now
 
-    $nextLines = Format-SkyOptionNextActionLines -Option $match -Slug $Slug
+    $nextLines = Format-SkyOptionNextActionLines -Option $match -Slug $Slug -Context $context
     Set-SkyJourneyPendingInteraction -JourneyPath $journeyPath -PendingYamlBlock $block `
         -NextActionsYamlLines $nextLines -ReplaceNextActions
 
@@ -121,7 +211,7 @@
         created_at = $createdAt
         resolved_at = $now
         routes_to = $(if ($match.routes_to) { $match.routes_to } else { $null })
-        command = $(if ($match.command) { ($match.command -replace '\{slug\}', $Slug) } else { $null })
+        command = $(if ($match.command) { Resolve-SkyInteractionTemplate -Template $match.command -Values $context } else { $null })
         agent_instruction = 'Seguir routes_to/comando do option escolhido; uma acao apenas.'
     }
     $payload | ConvertTo-Json -Depth 5
@@ -149,8 +239,9 @@
 }
 
 $now = (Get-Date).ToUniversalTime().ToString('o')
+$pendingMetadata = Get-SkyInteractionPendingMetadata -Slug $Slug -WorkspacePath $WorkspacePath -Stage $Stage -Options $opts
 $block = Format-SkyPendingInteractionYaml -PointId $(if ($PointId) { $PointId } else { 'custom' }) `
-    -Prompt $Prompt -Options $opts -Status 'pending' -Channel $Channel -CreatedAt $now
+    -Prompt $Prompt -Options $opts -Metadata $pendingMetadata -Status 'pending' -Channel $Channel -CreatedAt $now
 
 # Nao substituir next_suggested_actions ao abrir pending — preserva hints da sessao
 Set-SkyJourneyPendingInteraction -JourneyPath $journeyPath -PendingYamlBlock $block

diff --git a/scripts/sky/sky.ps1 b/scripts/sky/sky.ps1
--- a/scripts/sky/sky.ps1
+++ b/scripts/sky/sky.ps1
@@ -95,7 +95,13 @@
     [switch]$Clear,
 
     [Parameter()]
-    [switch]$Resolve
+    [switch]$Resolve,
+
+    [Parameter()]
+    [switch]$Assess,
+
+    [Parameter()]
+    [switch]$NoAssess
 )
 
 $ErrorActionPreference = 'Stop'
@@ -358,6 +364,8 @@
         if ($Slug) { $attachArgs.Slug = $Slug }
         if ($WorkspacePath) { $attachArgs.WorkspacePath = $WorkspacePath }
         if ($SyncMode) { $attachArgs.SyncMode = $SyncMode }
+        if ($Assess) { $attachArgs.Assess = $true }
+        if ($NoAssess) { $attachArgs.NoAssess = $true }
         if ($Force) { $attachArgs.Force = $true }
         & (Join-Path $PSScriptRoot 'attach-plugin.ps1') @attachArgs
         if ($Slug) {
@@ -382,6 +390,8 @@
     'interact' {
         if (-not $Slug) { throw 'interact requires -Slug' }
         $iArgs = @{ Slug = $Slug }
+        if ($WorkspacePath) { $iArgs.WorkspacePath = $WorkspacePath }
+        if ($Stage) { $iArgs.Stage = $Stage }
         if ($Clear) { $iArgs.Clear = $true }
         elseif ($Resolve) {
             $iArgs.Resolve = $true

You can send follow-ups to the cloud agent here.

Comment thread scripts/sky/prompt-interaction.ps1 Outdated
Comment thread scripts/sky/prompt-interaction.ps1
Comment thread scripts/sky/sky.ps1
Comment thread scripts/sky/attach-plugin.ps1 Outdated
@cursor

cursor Bot commented Jul 29, 2026

Copy link
Copy Markdown

Bugbot Autofix prepared fixes for all 4 issues found in the latest run.

  • ✅ Fixed: Workspace placeholder never substituted
    • The interaction loader now resolves catalog commands with slug/workspace/stage context and attach passes the workspace into post-attach HITL prompts.
  • ✅ Fixed: Sky attach hides NoAssess flag
    • Interaction resolution now applies option sets directives to session files, including disabling policies.open_to_elevation when elevate.confirm=disable is chosen.
  • ✅ Fixed: Deepen gap ignores maturity source
    • intake.deepen_gap now builds its options from session maturity top gaps and falls back to static options only when maturity data is unavailable.
  • ✅ Fixed: Duplicated interaction catalog mirror
    • Get-SkyInteractionCatalog now reads .agents/interaction-points.yaml directly so CLI behavior stays aligned with the documented interaction catalog.

Create PR

Or push these changes by commenting:

@cursor push 8227a3b2bd
Preview (8227a3b2bd)
diff --git a/scripts/sky/attach-plugin.ps1 b/scripts/sky/attach-plugin.ps1
--- a/scripts/sky/attach-plugin.ps1
+++ b/scripts/sky/attach-plugin.ps1
@@ -176,7 +176,7 @@
     if (Test-Path $interactScript) {
         Write-Host ""
         Write-Host "=== Proximo passo (interacao HITL) ===" -ForegroundColor Cyan
-        & $interactScript -Slug $Slug -PointId 'brownfield.after_attach' -ErrorAction SilentlyContinue
+        & $interactScript -Slug $Slug -PointId 'brownfield.after_attach' -WorkspacePath $workspace -ErrorAction SilentlyContinue
     }
 }
 

diff --git a/scripts/sky/interaction-catalog.ps1 b/scripts/sky/interaction-catalog.ps1
--- a/scripts/sky/interaction-catalog.ps1
+++ b/scripts/sky/interaction-catalog.ps1
@@ -1,100 +1,398 @@
 #Requires -Version 5.1
 <#
 .SYNOPSIS
-  Catálogo embutido de pontos de interação (espelho de .agents/interaction-points.yaml).
+  Carrega o catálogo de pontos de interação a partir de .agents/interaction-points.yaml.
   Dot-source: . (Join-Path $PSScriptRoot 'interaction-catalog.ps1')
 #>
 
+function ConvertFrom-SkyYamlScalar {
+    param([AllowNull()][string]$Value)
+
+    if ($null -eq $Value) { return $null }
+    $trimmed = $Value.Trim()
+    if ($trimmed -eq '') { return '' }
+
+    if (
+        ($trimmed.StartsWith('"') -and $trimmed.EndsWith('"')) -or
+        ($trimmed.StartsWith("'") -and $trimmed.EndsWith("'"))
+    ) {
+        return $trimmed.Substring(1, $trimmed.Length - 2)
+    }
+
+    switch -Regex ($trimmed) {
+        '^true$' { return $true }
+        '^false$' { return $false }
+        '^null$' { return $null }
+        '^-?\d+$' { return [int]$trimmed }
+        '^-?\d+\.\d+$' { return [double]$trimmed }
+        default { return $trimmed }
+    }
+}
+
+function ConvertTo-SkyInteractionHashtable {
+    param([Parameter(Mandatory = $true)]$InputObject)
+
+    $copy = [ordered]@{}
+    if ($InputObject -is [System.Collections.IDictionary]) {
+        foreach ($key in $InputObject.Keys) {
+            $copy[$key] = $InputObject[$key]
+        }
+    } else {
+        foreach ($prop in $InputObject.PSObject.Properties) {
+            $copy[$prop.Name] = $prop.Value
+        }
+    }
+    return $copy
+}
+
+function ConvertFrom-SkyYamlInlineList {
+    param([string]$Value)
+
+    $items = @()
+    if ([string]::IsNullOrWhiteSpace($Value)) { return $items }
+    $matches = [regex]::Matches($Value, '"([^"]*)"|''([^'']*)''|([^,\[\]]+)')
+    foreach ($match in $matches) {
+        $raw = if ($match.Groups[1].Success) {
+            $match.Groups[1].Value
+        } elseif ($match.Groups[2].Success) {
+            $match.Groups[2].Value
+        } else {
+            $match.Groups[3].Value.Trim()
+        }
+        if (-not [string]::IsNullOrWhiteSpace($raw)) {
+            $items += $raw.Trim()
+        }
+    }
+    return $items
+}
+
 function Get-SkyInteractionCatalog {
-    return @{
-        'arrival.intent' = @{
-            prompt = 'O que você quer fazer no Sky-Forge agora?'
-            options = @(
-                @{ id = 'new_idea'; label = 'Começar ideia nova (intake)'; routes_to = 'intake-conductor' }
-                @{ id = 'resume'; label = 'Retomar sessão existente'; routes_to = 'sky-host' }
-                @{ id = 'brownfield_repo'; label = 'Analisar / anexar repositório existente'; routes_to = 'intake-conductor' }
-                @{ id = 'status_only'; label = 'Só ver status de um projeto'; routes_to = 'sky-host' }
-            )
+    $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
+    $catalogPath = Join-Path $repoRoot '.agents\interaction-points.yaml'
+    if (-not (Test-Path $catalogPath)) {
+        throw "Catálogo de interações não encontrado: $catalogPath"
+    }
+
+    $catalog = @{}
+    $lines = @(Get-Content $catalogPath)
+    $inPoints = $false
+    $currentPoint = $null
+    $currentPointId = $null
+    $currentOption = $null
+    $currentList = $null
+
+    foreach ($line in $lines) {
+        if (-not $inPoints) {
+            if ($line -match '^points:\s*$') { $inPoints = $true }
+            continue
         }
-        'brownfield.path_confirm' = @{
-            prompt = 'Qual repositório devemos usar?'
-            options = @(
-                @{ id = 'current_workspace'; label = 'Este workspace aberto no Cursor' }
-                @{ id = 'paste_path'; label = 'Vou informar o caminho' }
-                @{ id = 'cancel'; label = 'Cancelar por agora' }
-            )
+
+        if ($line -match '^\s*$' -or $line -match '^\s*#') { continue }
+
+        if ($line -match '^  - id:\s*(.+)$') {
+            if ($currentOption -and $currentList) {
+                $currentPoint[$currentList] += ,$currentOption
+                $currentOption = $null
+            }
+            if ($currentPointId) {
+                $catalog[$currentPointId] = $currentPoint
+            }
+
+            $currentPointId = [string](ConvertFrom-SkyYamlScalar $Matches[1])
+            $currentPoint = @{
+                id = $currentPointId
+                options = @()
+                options_fallback = @()
+            }
+            $currentList = $null
+            continue
         }
-        'brownfield.after_attach' = @{
-            prompt = 'Host plugin anexado. Próximo passo?'
-            options = @(
-                @{ id = 'run_assess'; label = 'Rodar assessment do repositório'; command = './scripts/sky/sky.ps1 assess -Slug {slug} -WorkspacePath {workspace}' }
-                @{ id = 'deepen_problem'; label = 'Contar o problema de evolução (intake)'; routes_to = 'intake-conductor' }
-                @{ id = 'status'; label = 'Ver maturidade / status'; command = './scripts/sky/sky.ps1 status -Slug {slug}' }
-                @{ id = 'later'; label = 'Parar por aqui' }
-            )
+
+        if (-not $currentPoint) { continue }
+
+        if ($line -match '^    (options|options_fallback):\s*$') {
+            if ($currentOption -and $currentList) {
+                $currentPoint[$currentList] += ,$currentOption
+                $currentOption = $null
+            }
+            $currentList = $Matches[1]
+            continue
         }
-        'assess.next_action' = @{
-            prompt = 'Assessment pronto. O que prefere agora?'
-            options = @(
-                @{ id = 'deepen_top_gap'; label = 'Aprofundar a principal lacuna detectada'; routes_to = 'intake-conductor' }
-                @{ id = 'seed_roadmap'; label = 'Gerar roadmap de evolução (draft)'; command = './scripts/sky/sky.ps1 seed-roadmap -Slug {slug}' }
-                @{ id = 'elevate'; label = 'Explorar elevação / índices SKY'; routes_to = 'sky-elevator' }
-                @{ id = 'status'; label = 'Só revisar o status'; command = './scripts/sky/sky.ps1 status -Slug {slug}' }
-            )
+
+        if (($currentList -eq 'options' -or $currentList -eq 'options_fallback') -and $line -match '^      - id:\s*(.+)$') {
+            if ($currentOption) {
+                $currentPoint[$currentList] += ,$currentOption
+            }
+            $currentOption = @{ id = [string](ConvertFrom-SkyYamlScalar $Matches[1]) }
+            continue
         }
-        'intake.deepen_gap' = @{
-            prompt = 'Qual lacuna quer aprofundar agora?'
-            options = @(
-                @{ id = 'business'; label = 'Negócio / problema / stakeholders' }
-                @{ id = 'product'; label = 'Produto / jornadas / requisitos' }
-                @{ id = 'ux'; label = 'UX / acessibilidade' }
-                @{ id = 'something_else'; label = 'Outra coisa (vou digitar)' }
-            )
+
+        if ($currentOption -and ($currentList -eq 'options' -or $currentList -eq 'options_fallback') -and $line -match '^        ([a-zA-Z0-9_]+):\s*(.*)$') {
+            $currentOption[$Matches[1]] = ConvertFrom-SkyYamlScalar $Matches[2]
+            continue
         }
-        'elevate.confirm' = @{
-            prompt = 'Quer explorar conexões de elevação (opcional)?'
-            options = @(
-                @{ id = 'explore'; label = 'Sim, explorar sugestões'; routes_to = 'sky-elevator' }
-                @{ id = 'skip'; label = 'Não agora — seguir no produto'; routes_to = 'intake-conductor' }
-                @{ id = 'disable'; label = 'Prefiro não elevar neste projeto' }
-            )
+
+        if ($currentOption -and $line -match '^    [a-zA-Z0-9_]+:') {
+            $currentPoint[$currentList] += ,$currentOption
+            $currentOption = $null
+            $currentList = $null
         }
-        'gate.approve_stage' = @{
-            prompt = 'Há um gate humano pendente. Como seguir?'
-            options = @(
-                @{ id = 'approve'; label = 'Aprovar este stage agora'; command = './scripts/sky/sky.ps1 approve -Slug {slug} -Stage {stage}' }
-                @{ id = 'explain'; label = 'Explicar o que o gate protege' }
-                @{ id = 'defer'; label = 'Deixar para depois' }
-            )
+
+        if ($line -match '^    ([a-zA-Z0-9_]+):\s*(.*)$') {
+            $currentPoint[$Matches[1]] = ConvertFrom-SkyYamlScalar $Matches[2]
         }
-        'deliver.export_scope' = @{
-            prompt = 'Como quer o pacote de entrega?'
-            options = @(
-                @{ id = 'partial'; label = 'Parcial (pronto para handoff cedo)'; command = './scripts/sky/sky.ps1 export -Slug {slug} -Completeness partial' }
-                @{ id = 'full'; label = 'Completo (quando readiness permitir)'; command = './scripts/sky/sky.ps1 export -Slug {slug} -Completeness full' }
-                @{ id = 'for_ai'; label = 'Pacote para IA (-ForAI)'; command = './scripts/sky/sky.ps1 export -Slug {slug} -ForAI -Scope essential' }
-                @{ id = 'cancel'; label = 'Ainda não exportar' }
-            )
+    }
+
+    if ($currentOption -and $currentList) {
+        $currentPoint[$currentList] += ,$currentOption
+    }
+    if ($currentPointId) {
+        $catalog[$currentPointId] = $currentPoint
+    }
+
+    return $catalog
+}
+
+function Get-SkyInteractionTemplateContext {
+    param(
+        [string]$Slug = '',
+        [string]$WorkspacePath = '',
+        [string]$Stage = ''
+    )
+
+    $context = [ordered]@{}
+    if ($Slug) { $context.slug = $Slug }
+
+    $resolvedWorkspace = $WorkspacePath
+    if (-not $resolvedWorkspace -and $Slug) {
+        $linkScript = Join-Path $PSScriptRoot 'resolve-sky-link.ps1'
+        if (Test-Path $linkScript) {
+            . $linkScript
+            try {
+                $ctx = Get-SkyLinkContext -Slug $Slug
+                if ($ctx -and $ctx.WorkspacePath) {
+                    $resolvedWorkspace = $ctx.WorkspacePath
+                }
+            } catch {
+                $resolvedWorkspace = $null
+            }
         }
-        'showcase.privacy' = @{
-            prompt = 'Sobre privacidade do preview público…'
-            options = @(
-                @{ id = 'private_only'; label = 'Manter privado (só máquina / pasta externa)' }
-                @{ id = 'public_ok'; label = 'Autorizo publish -Public no showcase' }
-                @{ id = 'ask_later'; label = 'Decidir depois' }
-            )
+    }
+    if ($resolvedWorkspace) { $context.workspace = $resolvedWorkspace }
+    if ($Stage) { $context.stage = $Stage }
+
+    return $context
+}
+
+function Resolve-SkyInteractionTemplate {
+    param(
+        [AllowNull()][string]$Template,
+        [Parameter(Mandatory = $true)]$Context
+    )
+
+    if ($null -eq $Template) { return $null }
+    $resolved = $Template
+    foreach ($key in $Context.Keys) {
+        $resolved = $resolved.Replace("{$key}", [string]$Context[$key])
+    }
+    return $resolved
+}
+
+function Resolve-SkyInteractionOption {
+    param(
+        [Parameter(Mandatory = $true)]$Option,
+        [Parameter(Mandatory = $true)]$Context
+    )
+
+    $resolved = ConvertTo-SkyInteractionHashtable -InputObject $Option
+    if ($resolved.Contains('command') -and $resolved.command) {
+        $resolved.command = Resolve-SkyInteractionTemplate -Template $resolved.command -Context $Context
+    }
+    return $resolved
+}
+
+function Get-SkyInteractionDimensionLabels {
+    return @{
+        business = 'Negócio / problema / stakeholders'
+        product = 'Produto / jornadas / requisitos'
+        ux_design = 'UX / acessibilidade'
+        technical = 'Técnico / arquitetura / integrações'
+        sustainability = 'Sustentação / operações / ownership'
+        elevation = 'Elevação / conexões humanas'
+    }
+}
+
+function Format-SkyInteractionGapLabel {
+    param(
+        [Parameter(Mandatory = $true)][string]$Dimension,
+        [string]$Gap = ''
+    )
+
+    $labels = Get-SkyInteractionDimensionLabels
+    $label = if ($labels.ContainsKey($Dimension)) { $labels[$Dimension] } else { $Dimension }
+    if ($Gap) {
+        $label = "$label - $Gap"
+    }
+    if ($label.Length -gt 120) {
+        $label = $label.Substring(0, 117).TrimEnd() + '...'
+    }
+    return $label
+}
+
+function Get-SkyMaturityGapSummary {
+    param([string]$SessionDir)
+
+    $maturityPath = Join-Path $SessionDir 'maturity.yaml'
+    if (-not (Test-Path $maturityPath)) { return @() }
+
+    $weights = @{}
+    $dimensions = @{}
+    $section = ''
+    $currentDimension = $null
+    $capturingGaps = $false
+
+    foreach ($line in @(Get-Content $maturityPath)) {
+        if ($line -match '^weights:\s*$') {
+            $section = 'weights'
+            $currentDimension = $null
+            $capturingGaps = $false
+            continue
         }
-        'implement.agentic_repo' = @{
-            prompt = 'Recomendamos ARAH Harness neste repo. Quer instalar antes do scaffold/link?'
-            options = @(
-                @{ id = 'install_arah'; label = 'Sim — orientar instalação do ARAH Harness' }
-                @{ id = 'skip_arah'; label = 'Seguir sem ARAH por agora' }
-                @{ id = 'explain'; label = 'Explicar o que é o ARAH Harness' }
-            )
+        if ($line -match '^dimensions:\s*$') {
+            $section = 'dimensions'
+            $currentDimension = $null
+            $capturingGaps = $false
+            continue
         }
+        if ($line -match '^[a-zA-Z_][a-zA-Z0-9_]*:\s*$') {
+            $section = ''
+            $currentDimension = $null
+            $capturingGaps = $false
+            continue
+        }
+
+        switch ($section) {
+            'weights' {
+                if ($line -match '^  ([a-zA-Z0-9_]+):\s*([0-9.]+)\s*$') {
+                    $weights[$Matches[1]] = [double]$Matches[2]
+                }
+            }
+            'dimensions' {
+                if ($line -match '^  ([a-zA-Z0-9_]+):\s*$') {
+                    $currentDimension = $Matches[1]
+                    $dimensions[$currentDimension] = @{
+                        score = 0.0
+                        gaps = @()
+                    }
+                    $capturingGaps = $false
+                    continue
+                }
+                if (-not $currentDimension) { continue }
+
+                if ($line -match '^    score:\s*([0-9.]+)\s*$') {
+                    $dimensions[$currentDimension].score = [double]$Matches[1]
+                    continue
+                }
+                if ($line -match '^    gaps:\s*\[(.*)\]\s*$') {
+                    $dimensions[$currentDimension].gaps = @(ConvertFrom-SkyYamlInlineList -Value $Matches[1])
+                    $capturingGaps = $false
+                    continue
+                }
+                if ($line -match '^    gaps:\s*$') {
+                    $dimensions[$currentDimension].gaps = @()
+                    $capturingGaps = $true
+                    continue
+                }
+                if ($capturingGaps -and $line -match '^      -\s*(.+)$') {
+                    $dimensions[$currentDimension].gaps += [string](ConvertFrom-SkyYamlScalar $Matches[1])
+                    continue
+                }
+                if ($capturingGaps -and $line -notmatch '^      -') {
+                    $capturingGaps = $false
+                }
+            }
+        }
     }
+
+    $summary = @()
+    foreach ($dimension in $dimensions.Keys) {
+        $score = [double]$dimensions[$dimension].score
+        $weight = if ($weights.ContainsKey($dimension)) { [double]$weights[$dimension] } else { 1.0 }
+        $summary += [PSCustomObject]@{
+            dimension = $dimension
+            score = $score
+            weight = $weight
+            gaps = @($dimensions[$dimension].gaps)
+            priority = [Math]::Round((1 - $score) * $weight, 6)
+        }
+    }
+    return $summary
 }
 
+function Get-SkyMaturityTopGapOptions {
+    param(
+        [Parameter(Mandatory = $true)][string]$SessionDir,
+        $FallbackOptions = @()
+    )
+
+    $summary = @(Get-SkyMaturityGapSummary -SessionDir $SessionDir)
+    if ($summary.Count -eq 0) { return @($FallbackOptions) }
+
+    $dynamicOptions = @(
+        $summary |
+            Where-Object { $_.gaps.Count -gt 0 } |
+            Sort-Object -Property @{ Expression = 'priority'; Descending = $true }, @{ Expression = 'dimension'; Descending = $false } |
+            Select-Object -First 3 |
+            ForEach-Object {
+                @{
+                    id = $_.dimension
+                    label = Format-SkyInteractionGapLabel -Dimension $_.dimension -Gap $(if ($_.gaps.Count -gt 0) { $_.gaps[0] } else { '' })
+                }
+            }
+    )
+
+    if ($dynamicOptions.Count -eq 0) { return @($FallbackOptions) }
+
+    $freeform = @($FallbackOptions | Where-Object { $_.id -eq 'something_else' } | Select-Object -First 1)
+    if ($freeform.Count -gt 0) {
+        $dynamicOptions += ,(ConvertTo-SkyInteractionHashtable -InputObject $freeform[0])
+    }
+
+    return $dynamicOptions
+}
+
+function Get-SkyInteractionPoint {
+    param(
+        [Parameter(Mandatory = $true)][string]$PointId,
+        [string]$Slug = '',
+        [string]$WorkspacePath = '',
+        [string]$Stage = ''
+    )
+
+    $catalog = Get-SkyInteractionCatalog
+    if (-not $catalog.ContainsKey($PointId)) { return $null }
+
+    $entry = ConvertTo-SkyInteractionHashtable -InputObject $catalog[$PointId]
+    $context = Get-SkyInteractionTemplateContext -Slug $Slug -WorkspacePath $WorkspacePath -Stage $Stage
+
+    $options = @()
+    if ($entry.options_source -eq 'maturity.top_gaps' -and $Slug) {
+        $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
+        $sessionDir = Join-Path $repoRoot ".sky\sessions\$Slug"
+        $options = Get-SkyMaturityTopGapOptions -SessionDir $sessionDir -FallbackOptions @($entry.options_fallback)
+    } elseif ($entry.options -and $entry.options.Count -gt 0) {
+        $options = @($entry.options)
+    } else {
+        $options = @($entry.options_fallback)
+    }
+
+    if ($entry.Contains('prompt') -and $entry.prompt) {
+        $entry.prompt = Resolve-SkyInteractionTemplate -Template $entry.prompt -Context $context
+    }
+    $entry.options = @($options | ForEach-Object { Resolve-SkyInteractionOption -Option $_ -Context $context })
+    return $entry
+}
+
 function Get-SkyPendingInteractionSnapshot {
     <#
     .SYNOPSIS
@@ -109,6 +407,7 @@
         created_at = $null
         channel    = $null
         option_ids = @()
+        options    = @()
     }
 
     if ($JourneyRaw -notmatch '(?m)^pending_interaction\s*:') { return $snap }
@@ -134,23 +433,38 @@
     }
     $ids = [regex]::Matches($pendingChunk, '(?m)^    - id:\s*(\S+)')
     $snap.option_ids = @($ids | ForEach-Object { $_.Groups[1].Value })
+
+    $currentOption = $null
+    foreach ($line in ($pendingChunk -split '\r?\n')) {
+        if ($line -match '^    - id:\s*(\S+)') {
+            if ($currentOption) { $snap.options += ,$currentOption }
+            $currentOption = @{ id = $Matches[1].Trim() }
+            continue
+        }
+        if ($currentOption -and $line -match '^      ([a-zA-Z0-9_]+):\s*(.+)$') {
+            $currentOption[$Matches[1]] = ConvertFrom-SkyYamlScalar $Matches[2]
+        }
+    }
+    if ($currentOption) { $snap.options += ,$currentOption }
+
     return $snap
 }
 
 function Format-SkyOptionNextActionLines {
     param(
         [Parameter(Mandatory = $true)]$Option,
-        [string]$Slug = ''
+        [string]$Slug = '',
+        [string]$WorkspacePath = '',
+        [string]$Stage = ''
     )
+    $resolvedOption = Resolve-SkyInteractionOption -Option $Option -Context (Get-SkyInteractionTemplateContext -Slug $Slug -WorkspacePath $WorkspacePath -Stage $Stage)
     $lines = @()
-    $lines += "  - id: $($Option.id)"
-    $lbl = ($Option.label -replace '"', '''')
+    $lines += "  - id: $($resolvedOption.id)"
+    $lbl = ($resolvedOption.label -replace '"', '''')
     $lines += "    label: `"$lbl`""
-    if ($Option.routes_to) { $lines += "    agent: $($Option.routes_to)" }
-    if ($Option.command) {
-        $cmd = $Option.command
-        if ($Slug) { $cmd = $cmd -replace '\{slug\}', $Slug }
-        $lines += "    command: `"$($cmd -replace '"', '''')`""
+    if ($resolvedOption.routes_to) { $lines += "    agent: $($resolvedOption.routes_to)" }
+    if ($resolvedOption.command) {
+        $lines += "    command: `"$($resolvedOption.command -replace '"', '''')`""
     }
     return $lines
 }
@@ -203,7 +517,8 @@
             [void]$sb.AppendLine("      routes_to: $($opt.routes_to)")
         }
         if ($opt.command) {
-            [void]$sb.AppendLine("      command: `"$($opt.command)`"")
+            $cmd = $opt.command -replace '"', ''''
+            [void]$sb.AppendLine("      command: `"$cmd`"")
         }
     }
     return $sb.ToString().TrimEnd()

diff --git a/scripts/sky/prompt-interaction.ps1 b/scripts/sky/prompt-interaction.ps1
--- a/scripts/sky/prompt-interaction.ps1
+++ b/scripts/sky/prompt-interaction.ps1
@@ -30,6 +30,12 @@
     [string]$ChoiceId,
 
     [Parameter()]
+    [string]$WorkspacePath,
+
+    [Parameter()]
+    [string]$Stage,
+
+    [Parameter()]
     [string]$Channel = 'ask_question',
 
     [Parameter()]
@@ -48,6 +54,179 @@
 
 $rec = Join-Path $PSScriptRoot 'record-agent-event.ps1'
 
+function Merge-SkyInteractionOption {
+    param(
+        $Preferred,
+        $Fallback
+    )
+
+    if (-not $Preferred -and -not $Fallback) { return $null }
+    if (-not $Preferred) { return ConvertTo-SkyInteractionHashtable -InputObject $Fallback }
+    if (-not $Fallback) { return ConvertTo-SkyInteractionHashtable -InputObject $Preferred }
+
+    $merged = ConvertTo-SkyInteractionHashtable -InputObject $Fallback
+    $preferredMap = ConvertTo-SkyInteractionHashtable -InputObject $Preferred
+    foreach ($key in $preferredMap.Keys) {
+        $merged[$key] = $preferredMap[$key]
+    }
+    return $merged
+}
+
+function Test-SkyInteractionOptionHasPlaceholder {
+    param($Option)
+
+    if (-not $Option -or -not $Option.command) { return $false }
+    return [string]$Option.command -match '\{[a-zA-Z0-9_]+\}'
+}
+
+function Select-SkyInteractionOptions {
+    param(
+        [string]$OptionsJson,
+        $CatalogEntry,
+        $PendingOptions = @()
+    )
+
+    if ($OptionsJson) {
+        return @($OptionsJson | ConvertFrom-Json)
+    }
+
+    $catalogOptions = @()
+    if ($CatalogEntry -and $CatalogEntry.options) {
+        $catalogOptions = @($CatalogEntry.options)
+    }
+    if ($catalogOptions.Count -gt 0) {
+        $hasPlaceholder = $catalogOptions | Where-Object { Test-SkyInteractionOptionHasPlaceholder -Option $_ } | Select-Object -First 1
+        if (-not $hasPlaceholder) {
+            return $catalogOptions
+        }
+    }
+
+    if ($PendingOptions -and $PendingOptions.Count -gt 0) {
+        return @($PendingOptions)
+    }
+
+    return $catalogOptions
+}
+
+function ConvertTo-SkyYamlLiteral {
+    param($Value)
+
+    if ($null -eq $Value) { return 'null' }
+    if ($Value -is [bool]) { return $Value.ToString().ToLowerInvariant() }
+    if ($Value -is [int] -or $Value -is [long] -or $Value -is [double] -or $Value -is [decimal]) {
+        return [string]$Value
+    }
+
+    $text = [string]$Value
+    if ($text -match '^(true|false|null|-?\d+(\.\d+)?)$' -or $text -match '[:#\[\]\{\},]' -or $text -match '^\s' -or $text -match '\s$') {
+        return '"' + ($text -replace '"', '\"') + '"'
+    }
+    return $text
+}
+
+function Set-SkyYamlSectionScalar {
+    param(
+        [Parameter(Mandatory = $true)][string]$Path,
+        [Parameter(Mandatory = $true)][string]$Section,
+        [Parameter(Mandatory = $true)][string]$Key,
+        [Parameter(Mandatory = $true)]$Value
+    )
+
+    if (-not (Test-Path $Path)) {
+        throw "Arquivo não encontrado para aplicar sets: $Path"
+    }
+
+    $yamlValue = ConvertTo-SkyYamlLiteral -Value $Value
+    $lines = New-Object System.Collections.Generic.List[string]
+    foreach ($line in @(Get-Content $Path)) { $lines.Add($line) }
+
+    $sectionPattern = '^' + [regex]::Escape($Section) + ':\s*$'
+    $keyPattern = '^\s{2}' + [regex]::Escape($Key) + ':\s*'
+    $sectionIndex = -1
+    $sectionEnd = $lines.Count
+
+    for ($i = 0; $i -lt $lines.Count; $i++) {
+        if ($lines[$i] -match $sectionPattern) {
+            $sectionIndex = $i
+            $sectionEnd = $i + 1
+            while ($sectionEnd -lt $lines.Count -and ($lines[$sectionEnd] -match '^\s' -or $lines[$sectionEnd] -match '^\s*$')) {
+                $sectionEnd++
+            }
+            break
+        }
+    }
+
+    if ($sectionIndex -lt 0) {
+        if ($lines.Count -gt 0 -and $lines[$lines.Count - 1] -ne '') {
+            $lines.Add('')
+        }
+        $lines.Add("${Section}:")
+        $lines.Add("  ${Key}: $yamlValue")
+    } else {
+        $updated = $false
+        for ($i = $sectionIndex + 1; $i -lt $sectionEnd; $i++) {
+            if ($lines[$i] -match $keyPattern) {
+                $lines[$i] = "  ${Key}: $yamlValue"
+                $updated = $true
+                break
+            }
+        }
+        if (-not $updated) {
+            $lines.Insert($sectionEnd, "  ${Key}: $yamlValue")
+        }
+    }
+
+    Set-Content -Path $Path -Value (($lines -join "`n").TrimEnd() + "`n") -Encoding UTF8
+}
+
+function Apply-SkyInteractionSetExpression {
+    param(
+        [Parameter(Mandatory = $true)][string]$SessionDir,
+        [Parameter(Mandatory = $true)][string]$Expression
+    )
+
+    if ($Expression -notmatch '^([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)=(.+)$') {
+        throw "Expressão sets inválida: $Expression"
+    }
+
+    $section = $Matches[1]
+    $key = $Matches[2]
+    $value = ConvertFrom-SkyYamlScalar $Matches[3]
+
+    switch ($section) {
+        'policies' {
+            $targetPath = Join-Path $SessionDir 'sky-merits.yaml'
+        }
+        default {
+            throw "Section de sets não suportada: $section"
+        }
+    }
+
+    Set-SkyYamlSectionScalar -Path $targetPath -Section $section -Key $key -Value $value
+}
+
+function Apply-SkyInteractionOptionSets {
+    param(
+        [Parameter(Mandatory = $true)][string]$SessionDir,
+        $Option
+    )
+
+    if (-not $Option -or $null -eq $Option.sets) { return }
+
+    $setExpressions = @()
+    if ($Option.sets -is [string]) {
+        $setExpressions = @($Option.sets)
+    } else {
+        $setExpressions = @($Option.sets)
+    }
+
+    foreach ($expression in $setExpressions) {
+        if (-not [string]::IsNullOrWhiteSpace([string]$expression)) {
+            Apply-SkyInteractionSetExpression -SessionDir $SessionDir -Expression ([string]$expression)
+        }
+    }
+}
+
 if ($Clear) {
     Clear-SkyJourneyPendingInteraction -JourneyPath $journeyPath
     if (-not $NoAudit -and (Test-Path $rec)) {
@@ -65,12 +244,11 @@
     $snap = Get-SkyPendingInteractionSnapshot -JourneyRaw $raw
     $pointKey = if ($PointId) { $PointId } elseif ($snap.id) { $snap.id } else { 'unknown' }
 
-    $opts = @()
-    if ($OptionsJson) {
-        $opts = @($OptionsJson | ConvertFrom-Json)
-    } elseif ($catalog.ContainsKey($pointKey)) {
-        $opts = @($catalog[$pointKey].options)
+    $catalogEntry = $null
+    if ($catalog.ContainsKey($pointKey)) {
+        $catalogEntry = Get-SkyInteractionPoint -PointId $pointKey -Slug $Slug -WorkspacePath $WorkspacePath -Stage $Stage
     }
+    $opts = Select-SkyInteractionOptions -OptionsJson $OptionsJson -CatalogEntry $catalogEntry -PendingOptions $snap.options
 
     $match = $null
     if ($opts.Count -gt 0) {
@@ -86,24 +264,25 @@
         }
         throw "ChoiceId invalido: '$ChoiceId' para o ponto '$pointKey'. Opcoes: $known"
     }
+    if ($catalogEntry) {
+        $catalogMatch = $catalogEntry.options | Where-Object { $_.id -eq $ChoiceId } | Select-Object -First 1
+        $match = Merge-SkyInteractionOption -Preferred $match -Fallback $catalogMatch
+    }
 
     $choiceLabel = $match.label
     # Preservar prompt e created_at do pending em disco (ex.: override do assess)
-    $resolvedPrompt = if ($snap.prompt) { $snap.prompt } elseif ($catalog.ContainsKey($pointKey)) { $catalog[$pointKey].prompt } else { 'Decisao registrada.' }
+    $resolvedPrompt = if ($snap.prompt) { $snap.prompt } elseif ($catalogEntry) { $catalogEntry.prompt } else { 'Decisao registrada.' }
     $createdAt = if ($snap.created_at) { $snap.created_at } else { (Get-Date).ToUniversalTime().ToString('o') }
     $resolvedChannel = if ($snap.channel) { $snap.channel } else { $Channel }
     $now = (Get-Date).ToUniversalTime().ToString('o')
 
-    # Opcoes no bloco resolved: preferir catalogo (com command) se o ponto existir
-    if ($catalog.ContainsKey($pointKey)) {
-        $opts = @($catalog[$pointKey].options)
-    }
-
     $block = Format-SkyPendingInteractionYaml -PointId $pointKey -Prompt $resolvedPrompt -Options $opts `
         -Status 'resolved' -Channel $resolvedChannel -ChoiceId $ChoiceId -ChoiceLabel $choiceLabel `
         -CreatedAt $createdAt -ResolvedAt $now
 
-    $nextLines = Format-SkyOptionNextActionLines -Option $match -Slug $Slug
+    Apply-SkyInteractionOptionSets -SessionDir $sessionDir -Option $match
+
... diff truncated: showing 800 of 835 lines

You can send follow-ups to the cloud agent here.

Substitute {workspace}/{stage}, resolve from pending options, apply option sets, load maturity gaps for deepen_gap, forward -NoAssess, and clear stale pending only after attach succeeds.

Co-authored-by: Cursor <cursoragent@cursor.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 6 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 6 issues found in the latest run.

  • ✅ Fixed: Gate stage resolver picks wrong stage
    • Atualizei o resolvedor para considerar apenas stages realmente aprovados, aceitando tanto blocos approved: true quanto entradas escalares existentes.
  • ✅ Fixed: Deepen gap UX id mismatch
    • Corrigi o fallback para usar ux_design e passei a persistir na resolução as opções dinâmicas realmente exibidas ao usuário.
  • ✅ Fixed: Windows paths break YAML escaping
    • O escape YAML agora duplica barras invertidas corretamente e o unescape preserva caminhos Windows sem reinterpretar sequências como \n.
  • ✅ Fixed: Attach defaults to assess path
    • O attach só dispara assessment automático quando -Assess é solicitado explicitamente e -NoAssess não está ativo.
  • ✅ Fixed: Architecture gate not enforced
    • Incluí o gate architecture nas verificações de skill.invoke e side effects do check-autonomy, reutilizando leitura de aprovação compatível com o formato atual.
  • ✅ Fixed: Maturity gaps match weights keys
    • A seleção de top gaps agora restringe a busca ao bloco dimensions: e lê cada dimensão isoladamente, evitando colisões com weights.

Create PR

Or push these changes by commenting:

@cursor push 8b1d4d12a8
Preview (8b1d4d12a8)
diff --git a/.agents/interaction-points.yaml b/.agents/interaction-points.yaml
--- a/.agents/interaction-points.yaml
+++ b/.agents/interaction-points.yaml
@@ -106,7 +106,7 @@
         label: Negócio / problema / stakeholders
       - id: product
         label: Produto / jornadas / requisitos
-      - id: ux
+      - id: ux_design
         label: UX / acessibilidade
       - id: something_else
         label: Outra coisa (vou digitar)

diff --git a/scripts/sky/attach-plugin.ps1 b/scripts/sky/attach-plugin.ps1
--- a/scripts/sky/attach-plugin.ps1
+++ b/scripts/sky/attach-plugin.ps1
@@ -163,7 +163,7 @@
     }
 }
 
-$runAssess = $Assess -or -not $NoAssess
+$runAssess = $Assess -and -not $NoAssess
 
 # Só limpar pending herdado (arrival.intent) depois do attach/link bem-sucedido,
 # imediatamente antes do HITL pós-ação — evita sessão shape sem pergunta se o attach falhar no meio.

diff --git a/scripts/sky/check-autonomy.ps1 b/scripts/sky/check-autonomy.ps1
--- a/scripts/sky/check-autonomy.ps1
+++ b/scripts/sky/check-autonomy.ps1
@@ -28,11 +28,32 @@
 $allowed = $resolution.autonomy_check.allowed
 $gates = @($resolution.gates_required)
 
+function Test-SkyGateApproved {
+    param(
+        [string]$ApprovalsRaw,
+        [string]$Gate
+    )
+    if ([string]::IsNullOrWhiteSpace($ApprovalsRaw) -or [string]::IsNullOrWhiteSpace($Gate)) {
+        return $false
+    }
+    $escaped = [regex]::Escape($Gate)
+    if ($ApprovalsRaw -match "(?ms)^\s+$escaped`:\s*\r?\n(?:.*?\r?\n)*?\s+approved:\s*true\b") {
+        return $true
+    }
+    if ($ApprovalsRaw -match "(?m)^\s+$escaped`:\s*(.+?)\s*$") {
+        $value = $Matches[1].Trim().Trim('"').Trim("'")
+        if ($value -and $value -ne '{}' -and $value -ne 'null' -and $value -notmatch '^(false|pending|denied)$') {
+            return $true
+        }
+    }
+    return $false
+}
+
 $actionGates = @{
-    'export.package' = @('package')
-    'publish.preview' = @()
-    'publish.public' = @('public_showcase', 'package')
-    'skill.invoke' = @('brief')
+    'export.package' = @('architecture', 'package')
+    'publish.preview' = @('architecture')
+    'publish.public' = @('architecture', 'public_showcase', 'package')
+    'skill.invoke' = @('brief', 'architecture')
 }
 $allGates = [System.Collections.Generic.List[string]]::new()
 foreach ($g in $gates) { if ($allGates -notcontains $g) { $allGates.Add($g) } }
@@ -49,9 +70,7 @@
 if (Test-Path $approvalsPath) {
     $ap = Get-Content $approvalsPath -Raw
     foreach ($g in $gates) {
-        if ($ap -match "(?m)^\s+$g`:\s*approved") { $gateStatus[$g] = $true }
-        elseif ($ap -match "(?m)^\s+$g`:") { $gateStatus[$g] = $false }
-        else { $gateStatus[$g] = $false }
+        $gateStatus[$g] = Test-SkyGateApproved -ApprovalsRaw $ap -Gate $g
     }
 }
 

diff --git a/scripts/sky/interaction-catalog.ps1 b/scripts/sky/interaction-catalog.ps1
--- a/scripts/sky/interaction-catalog.ps1
+++ b/scripts/sky/interaction-catalog.ps1
@@ -8,7 +8,7 @@
 function Escape-SkyYamlDoubleQuoted {
     param([AllowNull()][string]$Text)
     if ($null -eq $Text) { return '' }
-    $t = $Text -replace '\\', '\\'
+    $t = $Text.Replace('\', '\\')
     $t = $t -replace '"', '\"'
     $t = $t -replace "`r`n", '\n'
     $t = $t -replace "`n", '\n'
@@ -19,9 +19,11 @@
 function Unescape-SkyYamlDoubleQuoted {
     param([AllowNull()][string]$Text)
     if ($null -eq $Text) { return '' }
-    $t = $Text -replace '\\n', "`n"
+    $sentinel = [string][char]0x1A
+    $t = $Text.Replace('\\', $sentinel)
+    $t = $t -replace '\\n', "`n"
     $t = $t -replace '\\"', '"'
-    $t = $t -replace '\\\\', '\'
+    $t = $t.Replace($sentinel, '\')
     return $t
 }
 
@@ -278,7 +280,17 @@
     }
     $raw = Get-Content $approvals -Raw
     foreach ($s in @('brief', 'elevation', 'architecture', 'package', 'public_showcase')) {
-        if ($raw -notmatch "(?m)^\s*${s}\s*:") {
+        $escaped = [regex]::Escape($s)
+        $approved = $false
+        if ($raw -match "(?ms)^\s+$escaped`:\s*\r?\n(?:.*?\r?\n)*?\s+approved:\s*true\b") {
+            $approved = $true
+        } elseif ($raw -match "(?m)^\s+$escaped`:\s*(.+?)\s*$") {
+            $value = $Matches[1].Trim().Trim('"').Trim("'")
+            if ($value -and $value -ne '{}' -and $value -ne 'null' -and $value -notmatch '^(false|pending|denied)$') {
+                $approved = $true
+            }
+        }
+        if (-not $approved) {
             return $s
         }
     }
@@ -289,6 +301,11 @@
     param([Parameter(Mandatory = $true)][string]$MaturityPath)
     if (-not (Test-Path $MaturityPath)) { return @() }
     $raw = Get-Content $MaturityPath -Raw
+    $dimensionsBlock = $null
+    if ($raw -match '(?ms)^dimensions\s*:\s*\r?\n(.*?)(?=^[A-Za-z_][A-Za-z0-9_]*\s*:|\z)') {
+        $dimensionsBlock = $Matches[1]
+    }
+    if (-not $dimensionsBlock) { return @() }
     $dimOrder = @('business', 'product', 'ux_design', 'technical', 'sustainability', 'elevation')
     $labels = @{
         business = 'Negócio'
@@ -300,12 +317,18 @@
     }
     $scored = @()
     foreach ($dim in $dimOrder) {
+        $escapedDim = [regex]::Escape($dim)
+        $dimBlock = $null
+        if ($dimensionsBlock -match "(?ms)^  $escapedDim`:\s*\r?\n(.*?)(?=^  [a-z_][a-z0-9_]*\s*:\s*\r?\n|\z)") {
+            $dimBlock = $Matches[1]
+        }
+        if (-not $dimBlock) { continue }
         $score = 1.0
-        if ($raw -match "(?ms)^\s+$dim`:\s*\r?\n(?:.*?\r?\n)*?\s+score:\s*([0-9.]+)") {
+        if ($dimBlock -match '(?m)^\s+score:\s*([0-9.]+)') {
             $score = [double]$Matches[1]
         }
         $gap = $null
-        if ($raw -match "(?ms)^\s+$dim`:\s*\r?\n(?:.*?\r?\n)*?\s+gaps:\s*\[([^\]]*)\]") {
+        if ($dimBlock -match '(?m)^\s+gaps:\s*\[([^\]]*)\]') {
             $inner = $Matches[1]
             if ($inner -match '"([^"]+)"') { $gap = $Matches[1] }
             elseif ($inner -match "'([^']+)'") { $gap = $Matches[1] }

diff --git a/scripts/sky/prompt-interaction.ps1 b/scripts/sky/prompt-interaction.ps1
--- a/scripts/sky/prompt-interaction.ps1
+++ b/scripts/sky/prompt-interaction.ps1
@@ -134,9 +134,7 @@
     $resolvedChannel = if ($snap.channel) { $snap.channel } else { $Channel }
     $now = (Get-Date).ToUniversalTime().ToString('o')
 
-    $optsForBlock = Expand-OptionCommands -Options $(
-        if ($catalog.ContainsKey($pointKey)) { $catalog[$pointKey].options } else { $opts }
-    )
+    $optsForBlock = Expand-OptionCommands -Options $opts
 
     $block = Format-SkyPendingInteractionYaml -PointId $pointKey -Prompt $resolvedPrompt -Options $optsForBlock `
         -Status 'resolved' -Channel $resolvedChannel -ChoiceId $ChoiceId -ChoiceLabel $choiceLabel `

You can send follow-ups to the cloud agent here.

Comment thread scripts/sky/interaction-catalog.ps1 Outdated
Comment thread scripts/sky/interaction-catalog.ps1
Comment thread scripts/sky/interaction-catalog.ps1
Comment thread .agents/interaction-points.yaml
Comment thread scripts/sky/attach-plugin.ps1
Comment thread .agents/autonomy.yaml

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
docs/schemas/interaction-turn.schema.yaml (1)

21-41: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Align pending_interaction options with the persisted schema.

scripts/sky/prompt-interaction.ps1 copies sets into options before Format-SkyPendingInteractionYaml, and that formatter writes option sets. docs/schemas/interaction-turn.schema.yaml does not allow sets, so valid pending_interaction entries can fail schema validation. Add the exact option sets catalog-compatible schema, or strip sets before persistence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/schemas/interaction-turn.schema.yaml` around lines 21 - 41, The pending
interaction option schema must accept the persisted sets field written by
Format-SkyPendingInteractionYaml after scripts/sky/prompt-interaction.ps1 copies
sets into options. Update the options item properties in the interaction-turn
schema to include sets with the exact catalog-compatible type and constraints,
preserving the existing id, label, routes_to, and command definitions.
scripts/sky/sky.ps1 (1)

97-104: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reject contradictory interaction modes before dispatch.

When -Clear and -Resolve are both supplied, the if ($Clear) branch silently wins, so a mistaken resolve command can clear the pending interaction instead. Reject conflicting modes—and irrelevant mode-specific arguments—before building $iArgs.

Also applies to: 390-408

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/sky/sky.ps1` around lines 97 - 104, Validate the parameter
combination in the script entry flow before building $iArgs or dispatching:
reject simultaneous -Clear and -Resolve, and reject arguments that are
irrelevant to the selected mode. Update the branching around $Clear/$Resolve and
the associated mode-specific parameters so conflicting or invalid combinations
fail explicitly instead of allowing the $Clear branch to win.
scripts/sky/new-session.ps1 (1)

1-1: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Save the PowerShell 5.1 scripts as UTF-8 with BOM. Both scripts/sky/new-session.ps1 and scripts/sky/sky.ps1 contain non-ASCII Portuguese strings, so PowerShell 5.1 can misdecode them from BOM-less UTF-8.

  • scripts/sky/new-session.ps1#L1-L1: save UTF-8 with BOM.
  • scripts/sky/sky.ps1#L1-L1: save UTF-8 with BOM.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/sky/new-session.ps1` at line 1, Save both
scripts/scripts/sky/new-session.ps1 (lines 1-1) and scripts/sky/sky.ps1 (lines
1-1) as UTF-8 with BOM, preserving their existing PowerShell content and
non-ASCII Portuguese strings.

Source: Linters/SAST tools

scripts/agents/choreograph-agents.ps1 (1)

1-13: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add a UTF-8 BOM to all affected PowerShell files.

PSScriptAnalyzer flags all three files, which contain non-ASCII Portuguese text.

  • scripts/agents/choreograph-agents.ps1#L1-L13: save with UTF-8 BOM.
  • scripts/sky/assess-platform.ps1#L194-L208: save with UTF-8 BOM.
  • scripts/sky/attach-plugin.ps1#L167-L185: save with UTF-8 BOM.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/agents/choreograph-agents.ps1` around lines 1 - 13, Save
scripts/agents/choreograph-agents.ps1, scripts/sky/assess-platform.ps1, and
scripts/sky/attach-plugin.ps1 as UTF-8 with a BOM, preserving their existing
content and behavior so PSScriptAnalyzer handles the non-ASCII Portuguese text
correctly.

Source: Linters/SAST tools

🧹 Nitpick comments (1)
scripts/sky/interaction-catalog.ps1 (1)

428-537: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate block-removal logic between Set-SkyJourneyPendingInteraction and Clear-SkyJourneyPendingInteraction.

Both functions implement the identical "find pending_interaction:, skip indented/blank lines until the next top-level key" loop (lines 444-455 and 516-527). Extracting a shared helper (e.g. Remove-SkyYamlTopLevelKeyBlock -Lines $lines -Key 'pending_interaction') would reduce the risk of the two copies silently diverging on a future edit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/sky/interaction-catalog.ps1` around lines 428 - 537, Extract the
duplicated pending_interaction block-removal loop from
Set-SkyJourneyPendingInteraction and Clear-SkyJourneyPendingInteraction into a
shared helper, such as Remove-SkyYamlTopLevelKeyBlock, accepting the lines and
key name. Replace both inline loops with that helper while preserving their
current handling of indented and blank lines and the existing output behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/schemas/interaction-turn.schema.yaml`:
- Around line 56-74: Update the resolved-status branch in the schema’s allOf
conditional to constrain then.properties.resolved_at to a non-null timestamp,
while preserving the existing required resolved_at and choice_id/freeform
validation paths.

In `@scripts/sky/assess-platform.ps1`:
- Around line 194-208: Route the HITL invocations in
scripts/sky/assess-platform.ps1 lines 194-208 and scripts/sky/attach-plugin.ps1
lines 175-185 through the audited sky.ps1 interact CLI entrypoint instead of
invoking prompt-interaction.ps1 directly. Preserve the existing point IDs,
prompts, and arguments for assess.next_action and brownfield.after_attach.

In `@scripts/sky/attach-plugin.ps1`:
- Around line 167-173: Update the pending-interaction handling around
Clear-SkyJourneyPendingInteraction so the existing pending_interaction remains
available until the replacement interaction is successfully persisted. Remove
the preemptive clearing before the post-attach flow, and ensure the successful
replacement overwrites the prior state while preserving or restoring it when the
subsequent command fails.

In `@scripts/sky/interaction-catalog.ps1`:
- Around line 8-26: Replace the chained replacement logic in
Unescape-SkyYamlDoubleQuoted with a single-pass tokenizer that consumes
recognized escape sequences left-to-right, decoding \n to a newline, \" to a
quote, and \\ to one backslash without allowing newly exposed characters to be
reinterpreted. Preserve null handling and ensure escaped backslashes followed by
a literal n round-trip unchanged.

In `@scripts/sky/new-session.ps1`:
- Around line 149-153: Update the initial interaction flow in new-session.ps1 to
persist the base session before recording arrival.intent, then invoke the
centralized sky.ps1 interact path with the session slug and point ID. Remove the
direct Get-SkyInteractionCatalog and Format-SkyPendingInteractionYaml
pending-block construction, ensuring the audited human.interaction.* lifecycle
is used.

In `@scripts/sky/prompt-interaction.ps1`:
- Around line 137-139: Update the $optsForBlock assignment to expand the
already-resolved $opts value directly instead of replacing it with
$catalog[$pointKey].options when the catalog contains the point. Preserve the
resolution order established earlier, so pending_interaction.options records the
exact elicited or persisted options while retaining their complete metadata.

In `@scripts/sky/sky.ps1`:
- Around line 49-53: Update the parameter validation in scripts/sky/sky.ps1 for
the Assess and NoAssess switches to reject invocations where both are supplied.
Fail immediately at the CLI boundary with a clear error message before
forwarding arguments to attach-plugin.ps1.

---

Outside diff comments:
In `@docs/schemas/interaction-turn.schema.yaml`:
- Around line 21-41: The pending interaction option schema must accept the
persisted sets field written by Format-SkyPendingInteractionYaml after
scripts/sky/prompt-interaction.ps1 copies sets into options. Update the options
item properties in the interaction-turn schema to include sets with the exact
catalog-compatible type and constraints, preserving the existing id, label,
routes_to, and command definitions.

In `@scripts/agents/choreograph-agents.ps1`:
- Around line 1-13: Save scripts/agents/choreograph-agents.ps1,
scripts/sky/assess-platform.ps1, and scripts/sky/attach-plugin.ps1 as UTF-8 with
a BOM, preserving their existing content and behavior so PSScriptAnalyzer
handles the non-ASCII Portuguese text correctly.

In `@scripts/sky/new-session.ps1`:
- Line 1: Save both scripts/scripts/sky/new-session.ps1 (lines 1-1) and
scripts/sky/sky.ps1 (lines 1-1) as UTF-8 with BOM, preserving their existing
PowerShell content and non-ASCII Portuguese strings.

In `@scripts/sky/sky.ps1`:
- Around line 97-104: Validate the parameter combination in the script entry
flow before building $iArgs or dispatching: reject simultaneous -Clear and
-Resolve, and reject arguments that are irrelevant to the selected mode. Update
the branching around $Clear/$Resolve and the associated mode-specific parameters
so conflicting or invalid combinations fail explicitly instead of allowing the
$Clear branch to win.

---

Nitpick comments:
In `@scripts/sky/interaction-catalog.ps1`:
- Around line 428-537: Extract the duplicated pending_interaction block-removal
loop from Set-SkyJourneyPendingInteraction and
Clear-SkyJourneyPendingInteraction into a shared helper, such as
Remove-SkyYamlTopLevelKeyBlock, accepting the lines and key name. Replace both
inline loops with that helper while preserving their current handling of
indented and blank lines and the existing output behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0d19d3e2-0c8d-459a-b9df-13b38e315ca0

📥 Commits

Reviewing files that changed from the base of the PR and between 1f6689b and 617077b.

📒 Files selected for processing (17)
  • .agents/autonomy.yaml
  • .agents/interaction-points.yaml
  • .agents/operational/sky-host.agent.yaml
  • .cursor/rules/sky-host.mdc
  • .cursor/rules/sky-intake.mdc
  • .cursor/skills/sky-experience/SKILL.md
  • .cursor/skills/sky-interact/SKILL.md
  • .skills/sky-interact.skill.yaml
  • docs/_meta/USER_JOURNEY.md
  • docs/schemas/interaction-turn.schema.yaml
  • scripts/agents/choreograph-agents.ps1
  • scripts/sky/assess-platform.ps1
  • scripts/sky/attach-plugin.ps1
  • scripts/sky/interaction-catalog.ps1
  • scripts/sky/new-session.ps1
  • scripts/sky/prompt-interaction.ps1
  • scripts/sky/sky.ps1
🚧 Files skipped from review as they are similar to previous changes (8)
  • .cursor/skills/sky-interact/SKILL.md
  • .skills/sky-interact.skill.yaml
  • .cursor/rules/sky-host.mdc
  • .agents/operational/sky-host.agent.yaml
  • docs/_meta/USER_JOURNEY.md
  • .agents/interaction-points.yaml
  • .cursor/rules/sky-intake.mdc
  • .cursor/skills/sky-experience/SKILL.md

Comment thread docs/schemas/interaction-turn.schema.yaml
Comment thread scripts/sky/assess-platform.ps1 Outdated
Comment thread scripts/sky/attach-plugin.ps1 Outdated
Comment thread scripts/sky/interaction-catalog.ps1
Comment thread scripts/sky/new-session.ps1 Outdated
Comment thread scripts/sky/prompt-interaction.ps1 Outdated
Comment thread scripts/sky/sky.ps1
sraphaz and others added 2 commits July 29, 2026 01:01
… defaults

Approve stage picks the first unapproved gate; Windows paths escape correctly; attach defaults to after_attach; deepen_gap uses ux_design; architecture gates enforce via autonomy checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
PS 5.1 and PSScriptAnalyzer need BOM for non-ASCII Portuguese strings in attach-plugin and interaction-catalog.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sraphaz
sraphaz merged commit feb38b4 into main Jul 29, 2026
4 checks passed

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Gate values misread as approved
    • Restrinjo a leitura ao bloco stages: e só considero o gate aprovado quando o valor é approved... ou um timestamp ISO.
  • ✅ Fixed: Export block cites wrong gate
    • sky export agora lê o JSON de check-autonomy.ps1 e expõe os gates realmente bloqueantes na mensagem, auditoria e erro.
  • ✅ Fixed: Elevation disable silently no-ops
    • A aplicação de sets agora falha explicitamente sem sky-merits.yaml ou sem bloco policies, e a resolução só é gravada após essa persistência.

Create PR

Or push these changes by commenting:

@cursor push f6e87b3636

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit b94b501. Configure here.

if ($ap -match "(?m)^\s+$([regex]::Escape($g)):\s*(approved|\d{4}-\d{2})") {
$gateStatus[$g] = $true
} elseif ($ap -match "(?m)^\s+$([regex]::Escape($g)):\s*\S+") {
$gateStatus[$g] = $true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gate values misread as approved

High Severity

The check-autonomy.ps1 script incorrectly interprets gate statuses from approvals.yaml. Its parsing logic treats any non-empty value for a gate (e.g., pending, denied) as an approval. This allows gated actions, such as export.package (which now requires architecture), to proceed without actual approval, bypassing security.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b94b501. Configure here.

Comment thread scripts/sky/sky.ps1
[switch]$Clear,

[Parameter()]
[switch]$Resolve

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Export block cites wrong gate

Medium Severity

When sky export is blocked by check-autonomy.ps1 for pending gates (like architecture), it incorrectly reports only the package gate as the blocker. This results in a misleading autonomy gate package error and directs users to approve only package, obscuring other blocking gates.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b94b501. Configure here.

$key = $Matches[1]
$val = $Matches[2].Trim()
$merits = Join-Path $SessionDir 'sky-merits.yaml'
if (-not (Test-Path $merits)) { return }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Elevation disable silently no-ops

Medium Severity

On interact -Resolve, Apply-SkyOptionSets returns without error when sky-merits.yaml is missing, while prompt-interaction.ps1 still reports success and emits sets in the JSON payload for choices like elevate.confirmdisable.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b94b501. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant