From 1f6689bb1e539eb0c565b0fb92139ce73d12049d Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 28 Jul 2026 23:06:29 -0300 Subject: [PATCH 1/6] feat(interact): HITL no Cursor com AskQuestion e sky interact 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 --- .agents/autonomy.yaml | 3 + .agents/choreography.yaml | 57 +++- .agents/interaction-points.yaml | 190 +++++++++++++ .../operational/intake-conductor.agent.yaml | 6 + .agents/operational/sky-host.agent.yaml | 9 +- .cursor/rules/sky-core.mdc | 1 + .cursor/rules/sky-host.mdc | 36 ++- .cursor/rules/sky-intake.mdc | 24 +- .cursor/skills/sky-experience/SKILL.md | 19 +- .cursor/skills/sky-interact/SKILL.md | 72 +++++ .skills/sky-interact.skill.yaml | 21 ++ AGENTS.md | 5 +- docs/_meta/AGENT_AUTONOMY.md | 2 + docs/_meta/AGENT_OBSERVABILITY.md | 1 + docs/_meta/SKY_INTERACT.md | 88 ++++++ docs/_meta/USER_JOURNEY.md | 20 ++ docs/schemas/interaction-turn.schema.yaml | 55 ++++ scripts/sky/assess-platform.ps1 | 14 + scripts/sky/attach-plugin.ps1 | 8 + scripts/sky/interaction-catalog.ps1 | 259 ++++++++++++++++++ scripts/sky/new-session.ps1 | 28 +- scripts/sky/prompt-interaction.ps1 | 161 +++++++++++ scripts/sky/sky.ps1 | 32 ++- templates/sessions/example-horta/journey.yaml | 23 ++ 24 files changed, 1102 insertions(+), 32 deletions(-) create mode 100644 .agents/interaction-points.yaml create mode 100644 .cursor/skills/sky-interact/SKILL.md create mode 100644 .skills/sky-interact.skill.yaml create mode 100644 docs/_meta/SKY_INTERACT.md create mode 100644 docs/schemas/interaction-turn.schema.yaml create mode 100644 scripts/sky/interaction-catalog.ps1 create mode 100644 scripts/sky/prompt-interaction.ps1 diff --git a/.agents/autonomy.yaml b/.agents/autonomy.yaml index 128fd13..3c08ca9 100644 --- a/.agents/autonomy.yaml +++ b/.agents/autonomy.yaml @@ -72,5 +72,8 @@ actions: - human.gate.requested - human.gate.approved - human.gate.denied + - human.interaction.requested + - human.interaction.answered + - human.interaction.cleared - choreography.resolve - autonomy.blocked diff --git a/.agents/choreography.yaml b/.agents/choreography.yaml index a95bad9..5f76f35 100644 --- a/.agents/choreography.yaml +++ b/.agents/choreography.yaml @@ -1,13 +1,16 @@ -# Coreografia — Sky-Forge v3 -# Autonomia explícita + gates + triggers para choreograph-agents.ps1 -version: 3 -updated: 2026-07-04 +# Coreografia — Sky-Forge v3.1 +# Autonomia explícita + gates + triggers + pontos de interação HITL +version: 3.1 +updated: 2026-07-28 policy: | Agentes operam dentro do nível de autonomia declarado. Ações acima do nível ou sem human gate são bloqueadas e registradas em .sky/audit/{slug}/events.jsonl. sky-host roteia; especialistas executam; domínio só consulta. + Decision points (.agents/interaction-points.yaml): pausar e elicitar com + AskQuestion (Cursor) ou fallback numerado antes de prosseguir. autonomy_ref: .agents/autonomy.yaml +interaction_points_ref: .agents/interaction-points.yaml triggers: - id: manual @@ -18,9 +21,37 @@ triggers: description: overall_readiness cruzou limiar - id: post_export description: Após export bem-sucedido + - 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) - id: hook_stop description: Hook Cursor stop — auditoria passiva +interaction_hooks: + - trigger: manual + when: arrival_or_unclear_intent + point: arrival.intent + agent: sky-host + - trigger: post_attach + point: brownfield.after_attach + agent: sky-host + - trigger: post_assess + point: assess.next_action + agent: sky-host + - trigger: maturity_gate + when: open_to_elevation + point: elevate.confirm + agent: sky-host + - trigger: manual + when: export_requested + point: deliver.export_scope + agent: delivery-steward + - trigger: manual + when: public_publish_requested + point: showcase.privacy + agent: showcase-curator + rules: - id: experience-host when: [manual, path_change] @@ -28,26 +59,29 @@ rules: - sky.config.yaml - docs/_meta/USER_JOURNEY.md - docs/_meta/OUTPUTS_AND_SHOWCASE.md + - docs/_meta/SKY_INTERACT.md + - .agents/interaction-points.yaml agents: - id: sky-host type: operational autonomy: [observe, route] max_autonomy: route + skills: [sky-interact] - id: delivery-steward type: operational autonomy: [observe, activate, invoke_skill] max_autonomy: invoke_skill - skills: [sky-deliver, sky-export, sky-validate] + skills: [sky-deliver, sky-export, sky-validate, sky-interact] requires_gate: [package] - id: showcase-curator type: operational autonomy: [observe, activate, invoke_skill] max_autonomy: invoke_skill - skills: [sky-publish, sky-showcase] + skills: [sky-publish, sky-showcase, sky-interact] requires_gate: [public_showcase] - id: intake-sessions - when: [manual, path_change] + when: [manual, path_change, post_assess, post_attach] paths: - .sky/sessions/** agents: @@ -55,11 +89,12 @@ rules: type: operational autonomy: [observe, route] max_autonomy: route + skills: [sky-interact] - id: intake-conductor type: operational autonomy: [observe, activate, invoke_skill] max_autonomy: activate - skills: [sky-intake, sky-approve, sky-elevate] + skills: [sky-intake, sky-approve, sky-elevate, sky-interact] requires_gate: [brief] - id: sky-elevator type: domain @@ -104,12 +139,13 @@ rules: type: operational autonomy: [observe, activate, invoke_skill, public] max_autonomy: public - skills: [sky-publish, sky-showcase] + skills: [sky-publish, sky-showcase, sky-interact] requires_gate: [public_showcase] - id: sky-host type: operational autonomy: [route] max_autonomy: route + skills: [sky-interact] - id: outputs-package when: [path_change, post_export, maturity_gate] @@ -121,7 +157,7 @@ rules: type: operational autonomy: [observe, activate, invoke_skill, side_effect] max_autonomy: side_effect - skills: [sky-deliver, sky-export, sky-validate] + skills: [sky-deliver, sky-export, sky-validate, sky-interact] requires_gate: [package] - id: showcase-curator type: operational @@ -265,6 +301,7 @@ party_mode: policy: | Reúne consultas paralelas de domínio. O host apresenta síntese — nunca export + publish no mesmo turno. Ativado por fase UX (journey.yaml current_phase). + Preferir AskQuestion para a decisão resultante da party. sessions: - id: elevation-party label: Elevação colaborativa diff --git a/.agents/interaction-points.yaml b/.agents/interaction-points.yaml new file mode 100644 index 0000000..540be86 --- /dev/null +++ b/.agents/interaction-points.yaml @@ -0,0 +1,190 @@ +# Pontos de interação HITL — Sky-Forge +# Agentes: preferir AskQuestion (Cursor UI); fallback = opções numeradas no chat. +# Fonte: docs/_meta/SKY_INTERACT.md · skill sky-interact +version: "1.0" +updated: 2026-07-28 + +policy: | + Em todo decision_point listado aqui, o agente DEVE pausar e elicitar + com AskQuestion quando a ferramenta estiver disponível no turno. + Se AskQuestion não existir (modelo/modo), usar o formato sky-host + (máx. 4 opções numeradas + "Responda com o número…"). + Uma pergunta por turno. Nunca empilhar gates no mesmo turno. + Após a escolha: atualizar journey.yaml (pending_interaction resolved + + next_suggested_actions) e registrar human.interaction.answered na auditoria. + +defaults: + max_options: 4 + allow_freeform_escape: true + freeform_label: "Outra coisa (vou digitar)" + ask_question_tool: AskQuestion + fallback: numbered_chat + +points: + - id: arrival.intent + phase: arrival + agent: sky-host + skill: sky-interact + when: Sem slug / chegada / usuário abre Sky-Forge sem pedido claro + 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 + + - id: brownfield.path_confirm + phase: shape + agent: sky-host + skill: sky-interact + when: Usuário pediu attach/assess sem WorkspacePath claro + 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 + + - id: brownfield.after_attach + phase: shape + agent: sky-host + skill: sky-interact + when: Após sky attach bem-sucedido + 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 + + - id: assess.next_action + phase: shape + agent: sky-host + skill: sky-interact + when: Após sky assess (platform-assessment.yaml gerado) + 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 + + - id: intake.deepen_gap + phase: shape + agent: intake-conductor + skill: sky-interact + when: Há lacunas de maturidade e o usuário não escolheu qual preencher + prompt: Qual lacuna quer aprofundar agora? + options_source: maturity.top_gaps + options_fallback: + - 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) + + - id: elevate.confirm + phase: elevate + agent: sky-host + skill: sky-interact + when: open_to_elevation true e sky-elevator tem sugestões ai_suggested + 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 + sets: policies.open_to_elevation=false + + - id: gate.approve_stage + phase: any + agent: sky-host + skill: sky-interact + when: Ação bloqueada por human gate (brief|architecture|package|public_showcase) + 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 + + - id: deliver.export_scope + phase: deliver + agent: delivery-steward + skill: sky-interact + when: Usuário pediu export sem escopo claro + 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) + - id: cancel + label: Ainda não exportar + + - id: showcase.privacy + phase: showcase + agent: showcase-curator + skill: sky-interact + when: Antes de publish -Public + 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 + requires_gate: public_showcase + - id: ask_later + label: Decidir depois + + - id: implement.agentic_repo + phase: implement + agent: repo-scaffolder + skill: sky-interact + when: agentic-repo-recommendation tier recommended|suggested + 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 diff --git a/.agents/operational/intake-conductor.agent.yaml b/.agents/operational/intake-conductor.agent.yaml index b2eee6a..36f27d6 100644 --- a/.agents/operational/intake-conductor.agent.yaml +++ b/.agents/operational/intake-conductor.agent.yaml @@ -15,6 +15,7 @@ skills: - sky-intake - sky-approve - sky-elevate + - sky-interact - sky-rag-query domain_consults: - sky-elevator @@ -47,6 +48,11 @@ elicitation: - invite_elevation_when_open_to_elevation - never_skip_business_minimum - handoff_to_delivery_when_readiness_partial + - prefer_ask_question_on_decision_points + interaction_points: + - intake.deepen_gap + - elevate.confirm + - assess.next_action guardrails: no_confirmed_requirements_without_user_ok: true respect_elevation_policy: true diff --git a/.agents/operational/sky-host.agent.yaml b/.agents/operational/sky-host.agent.yaml index b599c74..618dfdd 100644 --- a/.agents/operational/sky-host.agent.yaml +++ b/.agents/operational/sky-host.agent.yaml @@ -16,7 +16,7 @@ scope: - .sky/sessions/**/journey.yaml - .sky/sessions/**/maturity.yaml may_code: false -skills: [] +skills: [sky-interact] routes_to: - intake-conductor - delivery-steward @@ -29,10 +29,15 @@ ux: always_show_readiness: true always_show_phase: true language: mirror_user + prefer_ask_question: true + interaction_catalog: .agents/interaction-points.yaml forbid_in_same_turn: - export_and_publish - multiple_human_gates elicitation: + channel_order: [ask_question, numbered_chat] + decision_points_ref: .agents/interaction-points.yaml + arrival_point: arrival.intent arrival_prompts: - Tem uma ideia nova ou quer retomar um projeto? - Trouxe material existente (zip, docs, mockup)? @@ -40,7 +45,7 @@ elicitation: resume_template: | Bem-vindo de volta ao **{title}**. Maturidade: **{readiness}%** · Fase: **{phase_label}** - Próximos passos (escolha um número): + Próximos passos (AskQuestion ou escolha um número): guardrails: default_privacy: private confirm_before_public_publish: true diff --git a/.cursor/rules/sky-core.mdc b/.cursor/rules/sky-core.mdc index 3873a70..c633801 100644 --- a/.cursor/rules/sky-core.mdc +++ b/.cursor/rules/sky-core.mdc @@ -10,6 +10,7 @@ Leia [AGENTS.md](../../AGENTS.md) e [SKY_MERIT_INDICES.md](../../docs/_meta/SKY_ - Sessoes em `.sky/sessions/{slug}/` - Experiencia: **sky-host** — [USER_JOURNEY.md](../../docs/_meta/USER_JOURNEY.md) +- Interatividade HITL: skill **sky-interact** — [SKY_INTERACT.md](../../docs/_meta/SKY_INTERACT.md) · AskQuestion no Cursor - Intake: [INTAKE_PROTOCOL.md](../../docs/_meta/INTAKE_PROTOCOL.md) - Elevação: consultar `sky-elevator`; respeitar `sky-merits.yaml` → `policies` - UX: `ux-design-specialist` — WCAG AA, tokens, baixa excitação diff --git a/.cursor/rules/sky-host.mdc b/.cursor/rules/sky-host.mdc index ec7bcfc..2a98db2 100644 --- a/.cursor/rules/sky-host.mdc +++ b/.cursor/rules/sky-host.mdc @@ -1,9 +1,11 @@ --- -description: Sky Host — anfitrião da experiência; progresso, roteamento, uma decisão por vez +description: Sky Host — anfitrião da experiência; progresso, roteamento, elicitation HITL globs: - sky.config.yaml - docs/_meta/USER_JOURNEY.md + - docs/_meta/SKY_INTERACT.md - .sky/sessions/**/journey.yaml + - .agents/interaction-points.yaml alwaysApply: true --- @@ -14,10 +16,30 @@ Você é o **anfitrião** da experiência. O usuário não deve adivinhar o pró ## Antes de cada resposta 1. Se há slug: ler `.sky/sessions/{slug}/journey.yaml`, `maturity.yaml` e `agentic-repo-recommendation.yaml` (se existir) -2. Se não há slug: perguntar nome do projeto ou se quer retomar sessão existente -3. [USER_JOURNEY.md](../../docs/_meta/USER_JOURNEY.md) +2. Se `journey.yaml` tem `pending_interaction.status: pending` → **elicitar agora** (passo Interação) +3. Se não há slug: perguntar intenção (ponto `arrival.intent`) +4. [USER_JOURNEY.md](../../docs/_meta/USER_JOURNEY.md) · [SKY_INTERACT.md](../../docs/_meta/SKY_INTERACT.md) · skill `sky-interact` -## Formato de resposta (obrigatório) +## Interação HITL (obrigatório em decision points) + +Catálogo: [`.agents/interaction-points.yaml`](../../.agents/interaction-points.yaml) + +1. **Preferir `AskQuestion`** quando a ferramenta existir neste turno: + - Uma pergunta · até 4 opções · labels curtos + - Escape opcional: `Outra coisa (vou digitar)` + - Não duplicar as opções em prosa no mesmo turno +2. **Fallback** se AskQuestion indisponível — formato abaixo +3. Após a escolha: resolver via + `./scripts/sky/sky.ps1 interact -Slug {slug} -Resolve -ChoiceId {id}` + (ou atualizar `pending_interaction` no journey) e executar **uma** ação / handoff +4. CLI para gravar pending: + `./scripts/sky/sky.ps1 interact -Slug {slug} -PointId {point.id}` + +Pontos típicos: `arrival.intent`, `brownfield.after_attach`, `assess.next_action`, +`intake.deepen_gap`, `elevate.confirm`, `gate.approve_stage`, `deliver.export_scope`, +`showcase.privacy`, `implement.agentic_repo`. + +## Formato de resposta (fallback / contexto) ``` **{título do projeto ou "Sky-Forge"}** @@ -33,12 +55,14 @@ O que posso fazer agora: Responda com o número ou em suas palavras. ``` +Quando AskQuestion for usado, o cabeçalho maturidade/fase pode ficar numa linha curta **antes** da chamada à ferramenta; as opções ficam só no AskQuestion. + ## Roteamento | Situação | Agente | |----------|--------| | Ideia nova, lacunas, brownfield | intake-conductor | -| Repo existente — plugin host | `sky attach` → intake-conductor + assess | +| Repo existente — plugin host | `sky attach` → intake-conductor + assess → **HITL** `assess.next_action` | | Export, pasta externa, pacote | delivery-steward | | Site visual, publicar preview | showcase-curator | | Índices SKY, elevação | sky-elevator + ux-design-specialist | @@ -49,7 +73,7 @@ Responda com o número ou em suas palavras. Quando `agentic-repo-recommendation.yaml` tiver `tier: recommended` ou `suggested`: - Mencionar **ARAH Harness** como recomendação plausível (repo nosso — `sraphaz/arah-harness`) -- Oferecer instalação **antes** do scaffold ou `sky link`, não no mesmo turno que export +- Oferecer instalação **antes** do scaffold ou `sky link`, via ponto `implement.agentic_repo` (AskQuestion) - Catálogo: [agentic-repo.catalog.yaml](../../docs/recommendations/agentic-repo.catalog.yaml) - CLI: `./scripts/sky/sky.ps1 status -Slug ` regenera a avaliação diff --git a/.cursor/rules/sky-intake.mdc b/.cursor/rules/sky-intake.mdc index ad104fc..bdf3c37 100644 --- a/.cursor/rules/sky-intake.mdc +++ b/.cursor/rules/sky-intake.mdc @@ -1,5 +1,5 @@ --- -description: Intake-conductor Sky-Forge — conversa, maturidade, elevacao SKY e UX +description: Intake-conductor Sky-Forge — conversa, maturidade, elevacao SKY e UX, HITL globs: - .sky/sessions/** - templates/sessions/** @@ -12,9 +12,20 @@ Conduza elicitação dinâmica e **eleve** a proposta quando `policies.open_to_e ## Antes de cada resposta 1. Ler `.sky/sessions/{slug}/maturity.yaml` e `sky-merits.yaml` -2. Ler `brief-draft.yaml`, `alternatives.yaml`, `ux-spec.yaml` -3. Top 3 gaps ponderados (inclui `elevation` e `ux_design`) -4. [INTAKE_PROTOCOL.md](../../docs/_meta/INTAKE_PROTOCOL.md) +2. Ler `brief-draft.yaml`, `alternatives.yaml`, `ux-spec.yaml`, `journey.yaml` +3. Se `pending_interaction.status: pending` → skill **sky-interact** (AskQuestion / fallback) antes de escrever YAML novo +4. Top 3 gaps ponderados (inclui `elevation` e `ux_design`) +5. [INTAKE_PROTOCOL.md](../../docs/_meta/INTAKE_PROTOCOL.md) · [SKY_INTERACT.md](../../docs/_meta/SKY_INTERACT.md) + +## Interação ao aprofundar + +Quando houver lacunas e o usuário não escolheu qual preencher: + +1. Opcional: `./scripts/sky/sky.ps1 interact -Slug {slug} -PointId intake.deepen_gap` +2. Preferir **AskQuestion** com as top lacunas (máx. 4) — ponto `intake.deepen_gap` +3. Só então atualizar artefatos da dimensão escolhida + +Brownfield pós-assess: respeitar `assess.next_action` / `pending_interaction` do journey. ## Níveis @@ -30,7 +41,7 @@ Conduza elicitação dinâmica e **eleve** a proposta quando `policies.open_to_e > "Sua horta comunitária conecta-se a cuidado do lugar (CH-BIODIVERSITY). Quer explorar oficina com escolas (CH-EDUCATION) como pós-MVP?" -Sempre opcional. Se `open_to_elevation: false`, pule sugestões. +Sempre opcional. Elicitar com ponto `elevate.confirm` (AskQuestion). Se `open_to_elevation: false`, pule sugestões. ## Comandos @@ -38,6 +49,7 @@ Sempre opcional. Se `open_to_elevation: false`, pule sugestões. ./scripts/sky/sky.ps1 intake -Slug ./scripts/sky/sky.ps1 elevate -Slug ./scripts/sky/sky.ps1 approve -Slug -Stage elevation +./scripts/sky/sky.ps1 interact -Slug -PointId intake.deepen_gap ``` Persistir YAML a cada turno. Informar `SKY_SCORE` e `elevation_level` quando disponível. @@ -48,6 +60,6 @@ Se `overall_readiness ≥ 0.55`, oferecer handoff ao **delivery-steward** via ** | Agente | Quando | |--------|--------| -| sky-host | Sempre — face da experiência, opções numeradas | +| sky-host | Sempre — face da experiência, AskQuestion / opções | | delivery-steward | Export, pasta externa | | showcase-curator | Galeria visual, publish opt-in | diff --git a/.cursor/skills/sky-experience/SKILL.md b/.cursor/skills/sky-experience/SKILL.md index 31a9fdb..017c7ee 100644 --- a/.cursor/skills/sky-experience/SKILL.md +++ b/.cursor/skills/sky-experience/SKILL.md @@ -1,3 +1,11 @@ +--- +name: sky-experience +description: >- + Experiência Sky-Forge no Cursor — sky-host, export/showcase, retomada de + sessão e elicitation HITL (AskQuestion). Use when the user needs the next + step, export, showcase, resume, or interactive options mid-flow. +--- + # Sky Experience — Cursor skill Use quando o usuário interage com Sky-Forge sem saber o próximo passo, quer export/showcase, ou retoma sessão. @@ -13,12 +21,14 @@ Use quando o usuário interage com Sky-Forge sem saber o próximo passo, quer ex ## Antes de responder 1. `.sky/sessions/{slug}/journey.yaml` + `maturity.yaml` -2. [USER_JOURNEY.md](../../../docs/_meta/USER_JOURNEY.md) +2. Se `pending_interaction.status: pending` → carregar skill **sky-interact** e elicitar (AskQuestion / fallback) +3. [USER_JOURNEY.md](../../../docs/_meta/USER_JOURNEY.md) · [SKY_INTERACT.md](../../../docs/_meta/SKY_INTERACT.md) ## Formato (sky-host) - Maturidade % + fase humana -- Máximo 4 opções numeradas +- **AskQuestion** para decisões de opções fixas (preferido) +- Fallback: máximo 4 opções numeradas - Uma decisão por turno - Privacidade por padrão @@ -26,6 +36,7 @@ Use quando o usuário interage com Sky-Forge sem saber o próximo passo, quer ex ```powershell ./scripts/sky/sky.ps1 status -Slug +./scripts/sky/sky.ps1 interact -Slug -PointId arrival.intent ./scripts/sky/sky.ps1 export -Slug -Completeness partial ./scripts/sky/sky.ps1 publish -Slug -Public ./scripts/sky/sky.ps1 showcase @@ -34,5 +45,5 @@ Use quando o usuário interage com Sky-Forge sem saber o próximo passo, quer ex ## Regras - delivery-steward **não** publica no showcase -- showcase-curator **só** `-Public` com consentimento explícito -- Atualizar `journey.yaml` em cada handoff +- showcase-curator **só** `-Public` com consentimento explícito (`showcase.privacy`) +- Atualizar `journey.yaml` em cada handoff / resolução de interação diff --git a/.cursor/skills/sky-interact/SKILL.md b/.cursor/skills/sky-interact/SKILL.md new file mode 100644 index 0000000..e812d39 --- /dev/null +++ b/.cursor/skills/sky-interact/SKILL.md @@ -0,0 +1,72 @@ +--- +name: sky-interact +description: >- + Elicitação interativa Sky-Forge no Cursor (AskQuestion + fallback numerado). + Use when Sky-Forge needs a user decision, deepen an idea, attach/assess a + repo, approve a human gate, choose export/privacy options, or present + numbered choices — especially mid-agent flows that must pause for HITL. +--- + +# Sky Interact — UI de decisão no Cursor + +Pausa o fluxo Sky-Forge e elicita **uma** decisão do usuário com UI nativa +quando possível. + +## Quando usar (obrigatório) + +Qualquer `decision_point` em [`.agents/interaction-points.yaml`](../../../.agents/interaction-points.yaml), incluindo: + +- Chegada / intenção +- Attach / assess de repositório +- Aprofundar lacuna de maturidade +- Confirmar elevação +- Aprovar human gate +- Escopo de export / privacidade de showcase +- ARAH Harness antes de scaffold + +Também use quando o usuário pedir “opções”, “o que posso fazer”, ou quando +um agente estiver prestes a escolher sozinho entre caminhos divergentes. + +## Protocolo (ordem) + +1. **Ler contexto** — `journey.yaml`, `maturity.yaml`, e o `point` no catálogo. +2. **Preferir AskQuestion** — se a ferramenta `AskQuestion` estiver disponível neste turno: + - Uma pergunta por mensagem + - Até **4** opções (labels curtos) + - Incluir no máximo um escape freeform: `Outra coisa (vou digitar)` + - Não listar as mesmas opções em prosa no mesmo turno +3. **Fallback** — se AskQuestion indisponível (modelo/modo): + - Formato sky-host: maturidade · fase · opções `1)…4)` · “Responda com o número…” +4. **Após a resposta**: + - Atualizar `journey.yaml`: + - `pending_interaction.status: resolved` + - `pending_interaction.choice_id` / `choice_label` + - `next_suggested_actions` coerentes com a escolha + - Registrar auditoria: `human.interaction.answered` + - Seguir o `routes_to` / comando do catálogo (**uma** ação) +5. **CLI auxiliar** (opcional, grava o pending no disco): + +```powershell +./scripts/sky/sky.ps1 interact -Slug -PointId +./scripts/sky/sky.ps1 interact -Slug -Clear +``` + +## Anti-padrões + +- Export + publish no mesmo turno +- Vários AskQuestion no mesmo turno +- Prosseguir com side effect sem gate quando o catálogo exige pause +- Inventar >4 opções +- Usar AskQuestion para perguntas abertas longas (aí use prosa) + +## Canvas (opcional) + +Para resultados densos (ex.: assessment com muitos gaps), pode abrir um +Cursor Canvas **além** da decisão — a escolha acionável continua via +AskQuestion / fallback. Não substitua o HITL só com canvas. + +## Referências + +- [SKY_INTERACT.md](../../../docs/_meta/SKY_INTERACT.md) +- [USER_JOURNEY.md](../../../docs/_meta/USER_JOURNEY.md) +- [AGENT_AUTONOMY.md](../../../docs/_meta/AGENT_AUTONOMY.md) diff --git a/.skills/sky-interact.skill.yaml b/.skills/sky-interact.skill.yaml new file mode 100644 index 0000000..0cd07f6 --- /dev/null +++ b/.skills/sky-interact.skill.yaml @@ -0,0 +1,21 @@ +id: sky-interact +name: Sky Interact +description: > + Elicitação HITL no Cursor — AskQuestion (UI) com fallback numerado; + grava pending_interaction em journey.yaml. +inputs: + slug: kebab-case + point_id: id em .agents/interaction-points.yaml +steps: + - read: .agents/interaction-points.yaml + - read: docs/_meta/SKY_INTERACT.md + - read: .sky/sessions/{slug}/journey.yaml + - script: scripts/sky/prompt-interaction.ps1 -Slug {slug} -PointId {point_id} + - agent: > + Pausar. Preferir ferramenta AskQuestion com prompt/options do ponto. + Se indisponível, opções numeradas (sky-host). Após escolha, resolver + pending_interaction e rotear. +guardrails: + one_question_per_turn: true + max_options: 4 + never_stack_with: [export_and_publish, multiple_human_gates] diff --git a/AGENTS.md b/AGENTS.md index 7a541dc..7d8db8b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,6 +75,7 @@ Chegada (sky-host) → intake-conductor (conversa) | `sky-intake` | Sessão conversacional | | `sky-elevate` | Índices SKY + UX + humanity_connections | | `sky-approve` | Human gates | +| `sky-interact` | HITL no Cursor (AskQuestion + fallback) | | `sky-plan` | Pipeline batch (C4 + jornadas + craft) | | `sky-c4-model` | C4 três níveis + domínios | | `sky-journey-sequences` | Sequências + context-flow | @@ -123,6 +124,8 @@ Docs: [PROFILES.md](docs/_meta/PROFILES.md) · [PLUGINS.md](docs/_meta/PLUGINS.m ./scripts/sky/sky.ps1 link -Slug -WorkspacePath [-PullSpec] ./scripts/sky/sky.ps1 attach -WorkspacePath # host plugin ./scripts/sky/sky.ps1 assess -Slug -WorkspacePath +./scripts/sky/sky.ps1 interact -Slug -PointId arrival.intent +./scripts/sky/sky.ps1 interact -Slug -Resolve -ChoiceId ./scripts/sky/sky.ps1 pull-spec -Slug ./scripts/sky/sky.ps1 publish -Slug -Public ./scripts/sky/sky.ps1 showcase @@ -134,7 +137,7 @@ Docs: [PROFILES.md](docs/_meta/PROFILES.md) · [PLUGINS.md](docs/_meta/PLUGINS.m Autonomia e auditoria: [AGENT_AUTONOMY.md](docs/_meta/AGENT_AUTONOMY.md) · [AGENT_OBSERVABILITY.md](docs/_meta/AGENT_OBSERVABILITY.md) -Jornada UX: [USER_JOURNEY.md](docs/_meta/USER_JOURNEY.md) · Outputs externos: [OUTPUTS_AND_SHOWCASE.md](docs/_meta/OUTPUTS_AND_SHOWCASE.md) +Jornada UX: [USER_JOURNEY.md](docs/_meta/USER_JOURNEY.md) · Interatividade HITL: [SKY_INTERACT.md](docs/_meta/SKY_INTERACT.md) · Outputs externos: [OUTPUTS_AND_SHOWCASE.md](docs/_meta/OUTPUTS_AND_SHOWCASE.md) Compat: `./scripts/forge/forge.ps1` delega para `sky.ps1`. diff --git a/docs/_meta/AGENT_AUTONOMY.md b/docs/_meta/AGENT_AUTONOMY.md index a757482..0921a3f 100644 --- a/docs/_meta/AGENT_AUTONOMY.md +++ b/docs/_meta/AGENT_AUTONOMY.md @@ -25,6 +25,8 @@ Fonte: [`.agents/autonomy.yaml`](../../.agents/autonomy.yaml) | `package` | Export completo | | `public_showcase` | `-Public` no showcase | +Elicitação de “aprovar agora?” usa o ponto `gate.approve_stage` ([SKY_INTERACT.md](SKY_INTERACT.md)); a aprovação efetiva continua em: + ```powershell ./scripts/sky/sky.ps1 approve -Slug -Stage package ./scripts/sky/sky.ps1 approve -Slug -Stage public_showcase diff --git a/docs/_meta/AGENT_OBSERVABILITY.md b/docs/_meta/AGENT_OBSERVABILITY.md index 77320da..6d71977 100644 --- a/docs/_meta/AGENT_OBSERVABILITY.md +++ b/docs/_meta/AGENT_OBSERVABILITY.md @@ -81,6 +81,7 @@ Artefato: [`docs/_meta/agent-graph.generated.json`](agent-graph.generated.json) | `sky export` | `export.package` ok/blocked | | `sky publish` | `publish.preview` / `publish.public` | | `sky approve` | `human.gate.approved` | +| `sky interact` | `human.interaction.requested` / `answered` | | `choreograph-agents` | `choreography.resolve` | ## Privacidade diff --git a/docs/_meta/SKY_INTERACT.md b/docs/_meta/SKY_INTERACT.md new file mode 100644 index 0000000..8107efc --- /dev/null +++ b/docs/_meta/SKY_INTERACT.md @@ -0,0 +1,88 @@ +# Interatividade HITL — Sky-Forge no Cursor + +**Versão**: 1.0 | **Data**: 2026-07-28 + +Como o Sky-Forge **pausa e pergunta** ao usuário dentro do Cursor — com UI +nativa (`AskQuestion`) quando disponível — mesmo quando o fluxo foi iniciado +por um agente (`attach`, `assess`, intake, export…). + +## Objetivo + +O usuário não deve adivinhar o próximo passo **nem** digitar “1/2/3” se o +Cursor puder mostrar opções clicáveis. A jornada continua a mesma +([USER_JOURNEY.md](USER_JOURNEY.md)); muda o **canal** da elicitation. + +``` +Agente detecta decision_point + │ + ▼ +┌───────────────────┐ disponível ┌────────────────────┐ +│ Catálogo │ ───────────────────▶│ AskQuestion (UI) │ +│ interaction-points│ └─────────┬──────────┘ +└─────────┬─────────┘ │ + │ indisponível ▼ + ▼ Usuário escolhe +┌───────────────────┐ │ +│ Fallback numerado │◀──────────────────────────────┘ +│ (sky-host chat) │ +└─────────┬─────────┘ + ▼ + journey.pending_interaction → resolved + audit: human.interaction.answered + handoff / comando (uma ação) +``` + +## Fonte de verdade + +| Artefato | Função | +|----------|--------| +| [`.agents/interaction-points.yaml`](../../.agents/interaction-points.yaml) | Catálogo de pontos + prompts + opções | +| [`.cursor/skills/sky-interact/SKILL.md`](../../.cursor/skills/sky-interact/SKILL.md) | Protocolo para o agente Cursor | +| `journey.yaml` → `pending_interaction` | Estado da pergunta aberta / resolvida | +| [`interaction-turn.schema.yaml`](../schemas/interaction-turn.schema.yaml) | Contrato do bloco | +| `./scripts/sky/sky.ps1 interact` | Grava / limpa pending + imprime payload | + +## Regras + +1. **Uma decisão por turno** — alinhado ao sky-host. +2. **Máximo 4 opções** (+ no máximo um escape “Outra coisa…”). +3. **AskQuestion primeiro** — se a ferramenta existir no turno; senão fallback. +4. **Não empilhar** export + publish, nem múltiplos gates. +5. **Privacidade por padrão** — `showcase.privacy` antes de `-Public`. +6. **Auditoria** — `human.interaction.requested` / `human.interaction.answered`. + +## CLI + +```powershell +# Grava pending_interaction a partir do catálogo e imprime JSON para o agente +./scripts/sky/sky.ps1 interact -Slug minha-ideia -PointId arrival.intent + +# Pós-assess (também chamado automaticamente por assess-platform.ps1) +./scripts/sky/sky.ps1 interact -Slug minha-plataforma -PointId assess.next_action + +# Limpar pergunta pendente +./scripts/sky/sky.ps1 interact -Slug minha-ideia -Clear + +# Registrar escolha (após resposta do usuário) +./scripts/sky/sky.ps1 interact -Slug minha-ideia -Resolve -ChoiceId deepen_top_gap +``` + +## Integrações automáticas + +| Fluxo | PointId gravado | +|-------|-----------------| +| `sky attach` (sucesso) | `brownfield.after_attach` | +| `sky assess` (sucesso) | `assess.next_action` | +| `sky new-session` / intake | `arrival.intent` (em `next_suggested_actions` + pending opcional) | + +## Limitação do Cursor + +`AskQuestion` depende do **modelo/modo**. Se indisponível, o agente **deve** +cair no fallback numerado — não bloquear o fluxo nem fingir que perguntou. + +## Relação com human gates + +Interação ≠ aprovação formal. Gates (`brief`, `package`, `public_showcase`…) +continuam via `sky approve`. O ponto `gate.approve_stage` **elicita** se o +usuário quer aprovar agora; a aprovação efetiva permanece no CLI/skill +`sky-approve`. diff --git a/docs/_meta/USER_JOURNEY.md b/docs/_meta/USER_JOURNEY.md index ca20ca0..38f7df9 100644 --- a/docs/_meta/USER_JOURNEY.md +++ b/docs/_meta/USER_JOURNEY.md @@ -35,6 +35,8 @@ Mapa de experiência para **sky-host** e agentes especializados. O usuário nunc ## Mensagem tipo do sky-host (chegada) +Preferir **AskQuestion** (UI Cursor) com as mesmas opções. Fallback em prosa: + ``` Bem-vindo ao Sky-Forge. @@ -52,6 +54,24 @@ O que posso fazer por você agora (escolha uma): Responda com o número ou descreva em suas palavras. ``` +Catálogo de pontos e protocolo: [SKY_INTERACT.md](SKY_INTERACT.md) · `.agents/interaction-points.yaml` + +## Interatividade no Cursor (HITL) + +Após `attach` / `assess` / lacunas / gates, o agente **pausa** e elicita: + +1. Grava `pending_interaction` em `journey.yaml` (`sky interact -PointId …`) +2. Chama `AskQuestion` se disponível; senão lista numerada +3. Resolve com a escolha e segue **uma** ação + +| Gatilho | PointId | +|---------|---------| +| Chegada | `arrival.intent` | +| Pós-attach | `brownfield.after_attach` | +| Pós-assess | `assess.next_action` | +| Lacunas | `intake.deepen_gap` | +| Antes de `-Public` | `showcase.privacy` | + ## Handoffs entre agentes | De | Para | Quando | diff --git a/docs/schemas/interaction-turn.schema.yaml b/docs/schemas/interaction-turn.schema.yaml new file mode 100644 index 0000000..949279d --- /dev/null +++ b/docs/schemas/interaction-turn.schema.yaml @@ -0,0 +1,55 @@ +$schema: https://json-schema.org/draft/2020-12/schema +title: Sky-Forge interaction turn (pending_interaction in journey.yaml) +description: > + Contrato do bloco pending_interaction gravado em journey.yaml para + elicitações HITL (AskQuestion / fallback numerado). +type: object +additionalProperties: false +required: [id, status, prompt, options] +properties: + id: + type: string + description: Identificador do ponto (.agents/interaction-points.yaml) + pattern: '^[a-z][a-z0-9_.]*$' + status: + type: string + enum: [pending, resolved, cleared] + prompt: + type: string + minLength: 1 + maxLength: 500 + options: + type: array + minItems: 1 + maxItems: 5 + items: + type: object + additionalProperties: false + required: [id, label] + properties: + id: + type: string + label: + type: string + maxLength: 120 + routes_to: + type: string + command: + type: string + channel: + type: string + enum: [ask_question, numbered_chat, cli] + description: Canal usado na última elicitation + created_at: + type: string + format: date-time + resolved_at: + type: [string, 'null'] + format: date-time + choice_id: + type: [string, 'null'] + choice_label: + type: [string, 'null'] + freeform: + type: [string, 'null'] + description: Texto livre quando o usuário escolheu escape freeform diff --git a/scripts/sky/assess-platform.ps1 b/scripts/sky/assess-platform.ps1 index ad6657a..7f861a0 100644 --- a/scripts/sky/assess-platform.ps1 +++ b/scripts/sky/assess-platform.ps1 @@ -191,3 +191,17 @@ Write-Host " stacks: $stackNames | gaps: $($gaps.Count)" if ($gaps.Count -gt 0) { $gaps | ForEach-Object { Write-Host " - $_" -ForegroundColor DarkYellow } } + +# HITL: gravar pending_interaction para o agente Cursor (AskQuestion / fallback) +$interactScript = Join-Path $PSScriptRoot 'prompt-interaction.ps1' +if (Test-Path $interactScript) { + $topGap = if ($gaps.Count -gt 0) { $gaps[0] } else { $null } + $promptOverride = if ($topGap) { + "Assessment pronto (principal lacuna: $topGap). O que prefere agora?" + } else { + 'Assessment pronto. O que prefere agora?' + } + Write-Host "" + Write-Host "=== Proximo passo (interacao HITL) ===" -ForegroundColor Cyan + & $interactScript -Slug $Slug -PointId 'assess.next_action' -Prompt $promptOverride -ErrorAction SilentlyContinue +} diff --git a/scripts/sky/attach-plugin.ps1 b/scripts/sky/attach-plugin.ps1 index 6b1a580..d841450 100644 --- a/scripts/sky/attach-plugin.ps1 +++ b/scripts/sky/attach-plugin.ps1 @@ -167,6 +167,13 @@ $runAssess = $Assess -or -not $NoAssess if ($runAssess) { Write-Host "" & (Join-Path $PSScriptRoot 'assess-platform.ps1') -Slug $Slug -WorkspacePath $workspace +} else { + $interactScript = Join-Path $PSScriptRoot 'prompt-interaction.ps1' + if (Test-Path $interactScript) { + Write-Host "" + Write-Host "=== Proximo passo (interacao HITL) ===" -ForegroundColor Cyan + & $interactScript -Slug $Slug -PointId 'brownfield.after_attach' -ErrorAction SilentlyContinue + } } Write-Host "" @@ -179,3 +186,4 @@ Write-Host " ./scripts/sky.ps1 status" Write-Host " ./scripts/sky.ps1 assess" Write-Host "" Write-Host "Intake/evolucao: converse com sky-host no Cursor (profile $Profile)." +Write-Host "Interacao: se o agente Cursor tiver AskQuestion, use o pending_interaction do journey.yaml." diff --git a/scripts/sky/interaction-catalog.ps1 b/scripts/sky/interaction-catalog.ps1 new file mode 100644 index 0000000..5d98b3c --- /dev/null +++ b/scripts/sky/interaction-catalog.ps1 @@ -0,0 +1,259 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Catálogo embutido de pontos de interação (espelho de .agents/interaction-points.yaml). + Dot-source: . (Join-Path $PSScriptRoot 'interaction-catalog.ps1') +#> + +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' } + ) + } + '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' } + ) + } + 'brownfield.after_attach' = @{ + prompt = 'Host plugin anexado. Próximo passo?' + options = @( + @{ id = 'run_assess'; label = 'Rodar assessment do repositório' } + @{ id = 'deepen_problem'; label = 'Contar o problema de evolução (intake)'; routes_to = 'intake-conductor' } + @{ id = 'status'; label = 'Ver maturidade / status' } + @{ id = 'later'; label = 'Parar por aqui' } + ) + } + '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)' } + @{ id = 'elevate'; label = 'Explorar elevação / índices SKY'; routes_to = 'sky-elevator' } + @{ id = 'status'; label = 'Só revisar o status' } + ) + } + '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)' } + ) + } + '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' } + ) + } + 'gate.approve_stage' = @{ + prompt = 'Há um gate humano pendente. Como seguir?' + options = @( + @{ id = 'approve'; label = 'Aprovar este stage agora' } + @{ id = 'explain'; label = 'Explicar o que o gate protege' } + @{ id = 'defer'; label = 'Deixar para depois' } + ) + } + '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 = 'for_ai'; label = 'Pacote para IA (-ForAI)' } + @{ id = 'cancel'; label = 'Ainda não exportar' } + ) + } + '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' } + ) + } + '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' } + ) + } + } +} + +function Format-SkyPendingInteractionYaml { + param( + [Parameter(Mandatory = $true)][string]$PointId, + [Parameter(Mandatory = $true)][string]$Prompt, + [Parameter(Mandatory = $true)]$Options, + [string]$Status = 'pending', + [string]$Channel = 'ask_question', + [string]$ChoiceId = '', + [string]$ChoiceLabel = '', + [string]$CreatedAt = '', + [string]$ResolvedAt = '' + ) + if (-not $CreatedAt) { + $CreatedAt = (Get-Date).ToUniversalTime().ToString('o') + } + $sb = New-Object System.Text.StringBuilder + [void]$sb.AppendLine('pending_interaction:') + [void]$sb.AppendLine(" id: $PointId") + [void]$sb.AppendLine(" status: $Status") + [void]$sb.AppendLine(" channel: $Channel") + [void]$sb.AppendLine(" created_at: `"$CreatedAt`"") + if ($ResolvedAt) { + [void]$sb.AppendLine(" resolved_at: `"$ResolvedAt`"") + } else { + [void]$sb.AppendLine(' resolved_at: null') + } + if ($ChoiceId) { + [void]$sb.AppendLine(" choice_id: $ChoiceId") + } else { + [void]$sb.AppendLine(' choice_id: null') + } + if ($ChoiceLabel) { + $safe = $ChoiceLabel -replace '"', '''' + [void]$sb.AppendLine(" choice_label: `"$safe`"") + } else { + [void]$sb.AppendLine(' choice_label: null') + } + $promptSafe = ($Prompt -replace '"', '''') + [void]$sb.AppendLine(" prompt: `"$promptSafe`"") + [void]$sb.AppendLine(' options:') + foreach ($opt in $Options) { + [void]$sb.AppendLine(" - id: $($opt.id)") + $lbl = ($opt.label -replace '"', '''') + [void]$sb.AppendLine(" label: `"$lbl`"") + if ($opt.routes_to) { + [void]$sb.AppendLine(" routes_to: $($opt.routes_to)") + } + if ($opt.command) { + [void]$sb.AppendLine(" command: `"$($opt.command)`"") + } + } + return $sb.ToString().TrimEnd() +} + +function Set-SkyJourneyPendingInteraction { + param( + [Parameter(Mandatory = $true)][string]$JourneyPath, + [Parameter(Mandatory = $true)][string]$PendingYamlBlock, + [string[]]$NextActionsYamlLines = @() + ) + if (-not (Test-Path $JourneyPath)) { + throw "journey.yaml nao encontrado: $JourneyPath" + } + $now = (Get-Date).ToUniversalTime().ToString('o') + $lines = @(Get-Content $JourneyPath) + $out = New-Object System.Collections.Generic.List[string] + $i = 0 + $skipBlock = $null + while ($i -lt $lines.Count) { + $line = $lines[$i] + if ($line -match '^pending_interaction\s*:') { + $i++ + while ($i -lt $lines.Count -and ($lines[$i] -match '^\s' -or $lines[$i] -match '^\s*$')) { + if ($lines[$i] -match '^\s*$') { + $j = $i + 1 + while ($j -lt $lines.Count -and $lines[$j] -match '^\s*$') { $j++ } + if ($j -lt $lines.Count -and $lines[$j] -match '^[a-zA-Z_]') { break } + } + $i++ + } + continue + } + if ($line -match '^next_suggested_actions\s*:' -and $NextActionsYamlLines.Count -gt 0) { + $i++ + while ($i -lt $lines.Count -and ($lines[$i] -match '^\s' -or $lines[$i] -match '^\s*$')) { + if ($lines[$i] -match '^\s*$') { + $j = $i + 1 + while ($j -lt $lines.Count -and $lines[$j] -match '^\s*$') { $j++ } + if ($j -lt $lines.Count -and $lines[$j] -match '^[a-zA-Z_]') { break } + } + $i++ + } + $out.Add('next_suggested_actions:') + foreach ($l in $NextActionsYamlLines) { $out.Add($l) } + $out.Add('') + continue + } + if ($line -match '^updated_at:') { + $out.Add("updated_at: $now") + $i++ + continue + } + $out.Add($line) + $i++ + } + + $hasNext = $false + foreach ($l in $out) { if ($l -match '^next_suggested_actions:') { $hasNext = $true; break } } + if ($NextActionsYamlLines.Count -gt 0 -and -not $hasNext) { + $insertAt = $out.Count + for ($k = 0; $k -lt $out.Count; $k++) { + if ($out[$k] -match '^notes:') { $insertAt = $k; break } + } + $block = New-Object System.Collections.Generic.List[string] + $block.Add('next_suggested_actions:') + foreach ($l in $NextActionsYamlLines) { $block.Add($l) } + $block.Add('') + $out.InsertRange($insertAt, $block) + } + + while ($out.Count -gt 0 -and $out[$out.Count - 1] -match '^\s*$') { + $out.RemoveAt($out.Count - 1) + } + $out.Add('') + foreach ($l in ($PendingYamlBlock -split '\r?\n')) { + $out.Add($l) + } + $text = (($out -join "`n").TrimEnd() + "`n") + Set-Content -Path $JourneyPath -Value $text -Encoding UTF8 +} + +function Clear-SkyJourneyPendingInteraction { + param([Parameter(Mandatory = $true)][string]$JourneyPath) + if (-not (Test-Path $JourneyPath)) { return } + $now = (Get-Date).ToUniversalTime().ToString('o') + $lines = @(Get-Content $JourneyPath) + $out = New-Object System.Collections.Generic.List[string] + $i = 0 + while ($i -lt $lines.Count) { + $line = $lines[$i] + if ($line -match '^pending_interaction\s*:') { + $i++ + while ($i -lt $lines.Count -and ($lines[$i] -match '^\s' -or $lines[$i] -match '^\s*$')) { + if ($lines[$i] -match '^\s*$') { + $j = $i + 1 + while ($j -lt $lines.Count -and $lines[$j] -match '^\s*$') { $j++ } + if ($j -lt $lines.Count -and $lines[$j] -match '^[a-zA-Z_]') { break } + } + $i++ + } + continue + } + if ($line -match '^updated_at:') { + $out.Add("updated_at: $now") + $i++ + continue + } + $out.Add($line) + $i++ + } + Set-Content -Path $JourneyPath -Value (($out -join "`n").TrimEnd() + "`n") -Encoding UTF8 +} diff --git a/scripts/sky/new-session.ps1 b/scripts/sky/new-session.ps1 index 51df353..736e14f 100644 --- a/scripts/sky/new-session.ps1 +++ b/scripts/sky/new-session.ps1 @@ -169,9 +169,35 @@ next_suggested_actions: - id: brownfield label: Trouxe material existente (zip, docs, mockup) agent: intake-conductor + - id: brownfield_repo + label: Analisar / anexar repositório existente + agent: intake-conductor notes: | - Criado pelo sky-intake. sky-host conduz a experiência. + Criado pelo sky-intake. sky-host conduz via sky-interact (AskQuestion ou fallback numerado). + +pending_interaction: + id: arrival.intent + status: pending + channel: ask_question + created_at: "$now" + resolved_at: null + choice_id: null + choice_label: null + 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 "@ } Set-Content -Path $dest -Value $content -Encoding UTF8 -NoNewline diff --git a/scripts/sky/prompt-interaction.ps1 b/scripts/sky/prompt-interaction.ps1 new file mode 100644 index 0000000..ad072a6 --- /dev/null +++ b/scripts/sky/prompt-interaction.ps1 @@ -0,0 +1,161 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Grava / resolve / limpa pending_interaction em journey.yaml e imprime payload para o agente. +.EXAMPLE + ./scripts/sky/prompt-interaction.ps1 -Slug minha-ideia -PointId arrival.intent + ./scripts/sky/prompt-interaction.ps1 -Slug minha-ideia -Resolve -ChoiceId new_idea + ./scripts/sky/prompt-interaction.ps1 -Slug minha-ideia -Clear +#> +param( + [Parameter(Mandatory = $true)] + [string]$Slug, + + [Parameter()] + [string]$PointId, + + [Parameter()] + [string]$Prompt, + + [Parameter()] + [string]$OptionsJson, + + [Parameter()] + [switch]$Clear, + + [Parameter()] + [switch]$Resolve, + + [Parameter()] + [string]$ChoiceId, + + [Parameter()] + [string]$Channel = 'ask_question', + + [Parameter()] + [switch]$NoAudit +) + +$ErrorActionPreference = 'Stop' +$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..') +. (Join-Path $PSScriptRoot 'interaction-catalog.ps1') + +$sessionDir = Join-Path $RepoRoot ".sky\sessions\$Slug" +$journeyPath = Join-Path $sessionDir 'journey.yaml' +if (-not (Test-Path $journeyPath)) { + throw "Sessao nao encontrada: $Slug (faltando journey.yaml)" +} + +$rec = Join-Path $PSScriptRoot 'record-agent-event.ps1' + +if ($Clear) { + Clear-SkyJourneyPendingInteraction -JourneyPath $journeyPath + if (-not $NoAudit -and (Test-Path $rec)) { + & $rec -Slug $Slug -AgentId 'sky-host' -Action 'human.interaction.cleared' -Outcome 'ok' -AutonomyLevel 'route' -Details 'pending_interaction cleared' -ErrorAction SilentlyContinue | Out-Null + } + Write-Host "OK: pending_interaction limpo em $Slug" -ForegroundColor Green + return +} + +$catalog = Get-SkyInteractionCatalog + +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 + $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 + } elseif ($OptionsJson) { + $opts = $OptionsJson | ConvertFrom-Json + } else { + $opts = @(@{ id = $ChoiceId; label = $ChoiceId }) + } + if (-not $Prompt) { $Prompt = 'Decisao registrada.' } + $now = (Get-Date).ToUniversalTime().ToString('o') + $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 + 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 + } + Write-Host "OK: interacao resolvida ($pointKey -> $ChoiceId)" -ForegroundColor Green + $payload = [ordered]@{ + slug = $Slug + point_id = $pointKey + status = 'resolved' + choice_id = $ChoiceId + choice_label = $choiceLabel + agent_instruction = 'Seguir routes_to/comando do option escolhido; uma acao apenas.' + } + $payload | ConvertTo-Json -Depth 5 + return +} + +if (-not $PointId -and -not $Prompt) { + throw 'Informe -PointId (catalogo) ou -Prompt + -OptionsJson' +} + +$opts = $null +if ($PointId) { + if (-not $catalog.ContainsKey($PointId)) { + throw "PointId desconhecido: $PointId. Veja .agents/interaction-points.yaml" + } + $entry = $catalog[$PointId] + if (-not $Prompt) { $Prompt = $entry.prompt } + $opts = $entry.options +} +if ($OptionsJson) { + $opts = @($OptionsJson | ConvertFrom-Json) +} +if (-not $opts -or $opts.Count -eq 0) { + throw 'Nenhuma opcao para a interacao' +} + +$now = (Get-Date).ToUniversalTime().ToString('o') +$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)" } +} + +Set-SkyJourneyPendingInteraction -JourneyPath $journeyPath -PendingYamlBlock $block -NextActionsYamlLines $nextLines + +if (-not $NoAudit -and (Test-Path $rec)) { + & $rec -Slug $Slug -AgentId 'sky-host' -Action 'human.interaction.requested' -Outcome 'pending' ` + -AutonomyLevel 'route' -Details "point=$PointId" -ErrorAction SilentlyContinue | Out-Null +} + +Write-Host "OK: pending_interaction=$PointId em $Slug" -ForegroundColor Green +Write-Host "" +Write-Host "=== ASK_QUESTION_PAYLOAD (para o agente Cursor) ===" -ForegroundColor Cyan +$askPayload = [ordered]@{ + tool = 'AskQuestion' + slug = $Slug + point_id = $PointId + prompt = $Prompt + options = @($opts | ForEach-Object { + [ordered]@{ id = $_.id; label = $_.label } + }) + fallback = [ordered]@{ + format = 'numbered_chat' + instruction = 'Se AskQuestion indisponivel, listar 1..N no chat sky-host e pedir o numero.' + } + agent_instruction = 'Chamar AskQuestion AGORA com este prompt/options. Nao prosseguir side effects ate o usuario responder.' +} +$askPayload | ConvertTo-Json -Depth 6 diff --git a/scripts/sky/sky.ps1 b/scripts/sky/sky.ps1 index dbd106b..4e1496a 100644 --- a/scripts/sky/sky.ps1 +++ b/scripts/sky/sky.ps1 @@ -9,7 +9,7 @@ [CmdletBinding()] param( [Parameter(Position = 0, Mandatory = $true)] - [ValidateSet('intake', 'status', 'approve', 'run', 'validate', 'export', 'elevate', 'benchmark', 'publish', 'sync', 'showcase', 'agents', 'audit', 'choreograph', 'architect', 'link', 'link-sync', 'pull-spec', 'integrate-dc', 'visualize', 'attach', 'assess', 'seed-roadmap')] + [ValidateSet('intake', 'status', 'approve', 'run', 'validate', 'export', 'elevate', 'benchmark', 'publish', 'sync', 'showcase', 'agents', 'audit', 'choreograph', 'architect', 'link', 'link-sync', 'pull-spec', 'integrate-dc', 'visualize', 'attach', 'assess', 'seed-roadmap', 'interact')] [string]$Command, [Parameter()] @@ -80,7 +80,19 @@ param( [Parameter()] [ValidateSet('standard', 'showcase')] - [string]$Quality = 'standard' + [string]$Quality = 'standard', + + [Parameter()] + [string]$PointId, + + [Parameter()] + [string]$ChoiceId, + + [Parameter()] + [switch]$Clear, + + [Parameter()] + [switch]$Resolve ) $ErrorActionPreference = 'Stop' @@ -364,4 +376,20 @@ switch ($Command) { & (Join-Path $PSScriptRoot 'seed-evolution-roadmap.ps1') @seedArgs Invoke-AgentAudit $Slug 'solutions-architect' 'platform.seed_roadmap' 'invoke_skill' 'ok' } + 'interact' { + if (-not $Slug) { throw 'interact requires -Slug' } + $iArgs = @{ Slug = $Slug } + if ($Clear) { $iArgs.Clear = $true } + elseif ($Resolve) { + $iArgs.Resolve = $true + if (-not $ChoiceId) { throw 'interact -Resolve requires -ChoiceId' } + $iArgs.ChoiceId = $ChoiceId + if ($PointId) { $iArgs.PointId = $PointId } + } + else { + if (-not $PointId) { throw 'interact requires -PointId (ou -Clear / -Resolve)' } + $iArgs.PointId = $PointId + } + & (Join-Path $PSScriptRoot 'prompt-interaction.ps1') @iArgs + } } diff --git a/templates/sessions/example-horta/journey.yaml b/templates/sessions/example-horta/journey.yaml index 7d08a5f..22066e7 100644 --- a/templates/sessions/example-horta/journey.yaml +++ b/templates/sessions/example-horta/journey.yaml @@ -20,6 +20,29 @@ next_suggested_actions: - id: check_status label: Ver maturidade atual command: ./scripts/sky/sky.ps1 status -Slug example-horta + - id: deepen_gap + label: Escolher lacuna para aprofundar (HITL) + command: ./scripts/sky/sky.ps1 interact -Slug example-horta -PointId intake.deepen_gap + +pending_interaction: + id: intake.deepen_gap + status: pending + channel: ask_question + created_at: null + resolved_at: null + choice_id: null + choice_label: null + 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)" notes: | Atualizado pelo sky-host ou especialista ativo a cada handoff. + pending_interaction: skill sky-interact (AskQuestion ou fallback numerado). From 56b5303126f4838e573af5e13c4917fa6807a7a7 Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 28 Jul 2026 23:32:42 -0300 Subject: [PATCH 2/6] fix(interact): address Bugbot HITL catalog and resolve bugs Preserve journey hints and pending timestamps, validate ChoiceId, emit option commands, and clear stale arrival pending on attach. Co-authored-by: Cursor --- .agents/interaction-points.yaml | 2 + scripts/sky/attach-plugin.ps1 | 4 ++ scripts/sky/interaction-catalog.ps1 | 106 ++++++++++++++++++++++------ scripts/sky/prompt-interaction.ps1 | 82 +++++++++++++-------- scripts/sky/sky.ps1 | 4 ++ 5 files changed, 146 insertions(+), 52 deletions(-) diff --git a/.agents/interaction-points.yaml b/.agents/interaction-points.yaml index 540be86..aea62d7 100644 --- a/.agents/interaction-points.yaml +++ b/.agents/interaction-points.yaml @@ -92,6 +92,7 @@ points: 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 @@ -157,6 +158,7 @@ points: 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 diff --git a/scripts/sky/attach-plugin.ps1 b/scripts/sky/attach-plugin.ps1 index d841450..3f7132d 100644 --- a/scripts/sky/attach-plugin.ps1 +++ b/scripts/sky/attach-plugin.ps1 @@ -83,6 +83,10 @@ if (Test-Path $journeyPath) { $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' diff --git a/scripts/sky/interaction-catalog.ps1 b/scripts/sky/interaction-catalog.ps1 index 5d98b3c..6b32ae8 100644 --- a/scripts/sky/interaction-catalog.ps1 +++ b/scripts/sky/interaction-catalog.ps1 @@ -27,9 +27,9 @@ function Get-SkyInteractionCatalog { '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 @@ function Get-SkyInteractionCatalog { 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 @@ function Get-SkyInteractionCatalog { '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,9 +70,9 @@ function Get-SkyInteractionCatalog { '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 = 'for_ai'; label = 'Pacote para IA (-ForAI)' } + @{ 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' } ) } @@ -95,6 +95,66 @@ function Get-SkyInteractionCatalog { } } +function Get-SkyPendingInteractionSnapshot { + <# + .SYNOPSIS + Lê campos do bloco pending_interaction atual em journey.yaml (texto bruto). + #> + param([Parameter(Mandatory = $true)][string]$JourneyRaw) + + $snap = @{ + id = $null + status = $null + prompt = $null + created_at = $null + channel = $null + option_ids = @() + } + + if ($JourneyRaw -notmatch '(?m)^pending_interaction\s*:') { return $snap } + + $pendingChunk = $null + if ($JourneyRaw -match '(?ms)^pending_interaction:\r?\n(.*?)(?=^[a-zA-Z_]|\z)') { + $pendingChunk = $Matches[1] + } + 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('"') + } + if ($pendingChunk -match '(?m)^ prompt:\s*"((?:\\.|[^"])*)"') { + $snap.prompt = $Matches[1] -replace "''", "'" + } elseif ($pendingChunk -match '(?m)^ prompt:\s*(.+)$') { + $snap.prompt = $Matches[1].Trim().Trim('"') + } + $ids = [regex]::Matches($pendingChunk, '(?m)^ - id:\s*(\S+)') + $snap.option_ids = @($ids | ForEach-Object { $_.Groups[1].Value }) + return $snap +} + +function Format-SkyOptionNextActionLines { + param( + [Parameter(Mandatory = $true)]$Option, + [string]$Slug = '' + ) + $lines = @() + $lines += " - id: $($Option.id)" + $lbl = ($Option.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 '"', '''')`"" + } + return $lines +} + function Format-SkyPendingInteractionYaml { param( [Parameter(Mandatory = $true)][string]$PointId, @@ -153,7 +213,8 @@ function Set-SkyJourneyPendingInteraction { param( [Parameter(Mandatory = $true)][string]$JourneyPath, [Parameter(Mandatory = $true)][string]$PendingYamlBlock, - [string[]]$NextActionsYamlLines = @() + [string[]]$NextActionsYamlLines = @(), + [switch]$ReplaceNextActions ) if (-not (Test-Path $JourneyPath)) { throw "journey.yaml nao encontrado: $JourneyPath" @@ -162,7 +223,6 @@ function Set-SkyJourneyPendingInteraction { $lines = @(Get-Content $JourneyPath) $out = New-Object System.Collections.Generic.List[string] $i = 0 - $skipBlock = $null while ($i -lt $lines.Count) { $line = $lines[$i] if ($line -match '^pending_interaction\s*:') { @@ -177,7 +237,7 @@ function Set-SkyJourneyPendingInteraction { } continue } - if ($line -match '^next_suggested_actions\s*:' -and $NextActionsYamlLines.Count -gt 0) { + if ($line -match '^next_suggested_actions\s*:' -and $ReplaceNextActions -and $NextActionsYamlLines.Count -gt 0) { $i++ while ($i -lt $lines.Count -and ($lines[$i] -match '^\s' -or $lines[$i] -match '^\s*$')) { if ($lines[$i] -match '^\s*$') { @@ -201,18 +261,20 @@ function Set-SkyJourneyPendingInteraction { $i++ } - $hasNext = $false - foreach ($l in $out) { if ($l -match '^next_suggested_actions:') { $hasNext = $true; break } } - if ($NextActionsYamlLines.Count -gt 0 -and -not $hasNext) { - $insertAt = $out.Count - for ($k = 0; $k -lt $out.Count; $k++) { - if ($out[$k] -match '^notes:') { $insertAt = $k; break } + if ($ReplaceNextActions -and $NextActionsYamlLines.Count -gt 0) { + $hasNext = $false + foreach ($l in $out) { if ($l -match '^next_suggested_actions:') { $hasNext = $true; break } } + if (-not $hasNext) { + $insertAt = $out.Count + for ($k = 0; $k -lt $out.Count; $k++) { + if ($out[$k] -match '^notes:') { $insertAt = $k; break } + } + $block = New-Object System.Collections.Generic.List[string] + $block.Add('next_suggested_actions:') + foreach ($l in $NextActionsYamlLines) { $block.Add($l) } + $block.Add('') + $out.InsertRange($insertAt, $block) } - $block = New-Object System.Collections.Generic.List[string] - $block.Add('next_suggested_actions:') - foreach ($l in $NextActionsYamlLines) { $block.Add($l) } - $block.Add('') - $out.InsertRange($insertAt, $block) } while ($out.Count -gt 0 -and $out[$out.Count - 1] -match '^\s*$') { diff --git a/scripts/sky/prompt-interaction.ps1 b/scripts/sky/prompt-interaction.ps1 index ad072a6..f2f7c57 100644 --- a/scripts/sky/prompt-interaction.ps1 +++ b/scripts/sky/prompt-interaction.ps1 @@ -62,28 +62,51 @@ $catalog = Get-SkyInteractionCatalog 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] + $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) } - $choiceLabel = $ChoiceId - $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 - } elseif ($OptionsJson) { - $opts = $OptionsJson | ConvertFrom-Json - } else { - $opts = @(@{ id = $ChoiceId; label = $ChoiceId }) + + $match = $null + if ($opts.Count -gt 0) { + $match = $opts | Where-Object { $_.id -eq $ChoiceId } | Select-Object -First 1 + } + if (-not $match) { + $known = if ($opts.Count -gt 0) { + ($opts | ForEach-Object { $_.id }) -join ', ' + } elseif ($snap.option_ids.Count -gt 0) { + $snap.option_ids -join ', ' + } else { + '(nenhuma)' + } + throw "ChoiceId invalido: '$ChoiceId' para o ponto '$pointKey'. Opcoes: $known" } - if (-not $Prompt) { $Prompt = 'Decisao registrada.' } + + $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.' } + $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') - $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 + + # 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 + Set-SkyJourneyPendingInteraction -JourneyPath $journeyPath -PendingYamlBlock $block ` + -NextActionsYamlLines $nextLines -ReplaceNextActions + 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 @@ -95,6 +118,10 @@ if ($Resolve) { status = 'resolved' choice_id = $ChoiceId choice_label = $choiceLabel + 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 }) agent_instruction = 'Seguir routes_to/comando do option escolhido; uma acao apenas.' } $payload | ConvertTo-Json -Depth 5 @@ -125,16 +152,8 @@ $now = (Get-Date).ToUniversalTime().ToString('o') $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)" } -} - -Set-SkyJourneyPendingInteraction -JourneyPath $journeyPath -PendingYamlBlock $block -NextActionsYamlLines $nextLines +# Nao substituir next_suggested_actions ao abrir pending — preserva hints da sessao +Set-SkyJourneyPendingInteraction -JourneyPath $journeyPath -PendingYamlBlock $block if (-not $NoAudit -and (Test-Path $rec)) { & $rec -Slug $Slug -AgentId 'sky-host' -Action 'human.interaction.requested' -Outcome 'pending' ` @@ -150,7 +169,10 @@ $askPayload = [ordered]@{ point_id = $PointId prompt = $Prompt options = @($opts | ForEach-Object { - [ordered]@{ id = $_.id; label = $_.label } + $o = [ordered]@{ id = $_.id; label = $_.label } + if ($_.routes_to) { $o.routes_to = $_.routes_to } + if ($_.command) { $o.command = $_.command } + $o }) fallback = [ordered]@{ format = 'numbered_chat' diff --git a/scripts/sky/sky.ps1 b/scripts/sky/sky.ps1 index 4e1496a..433b8cc 100644 --- a/scripts/sky/sky.ps1 +++ b/scripts/sky/sky.ps1 @@ -88,6 +88,9 @@ param( [Parameter()] [string]$ChoiceId, + [Parameter()] + [string]$Prompt, + [Parameter()] [switch]$Clear, @@ -389,6 +392,7 @@ switch ($Command) { else { if (-not $PointId) { throw 'interact requires -PointId (ou -Clear / -Resolve)' } $iArgs.PointId = $PointId + if ($Prompt) { $iArgs.Prompt = $Prompt } } & (Join-Path $PSScriptRoot 'prompt-interaction.ps1') @iArgs } From 729eae7ad393ea3576f47038d57b92e53bf8471c Mon Sep 17 00:00:00 2001 From: Raphael Date: Tue, 28 Jul 2026 23:36:35 -0300 Subject: [PATCH 3/6] fix(interact): address CodeRabbit HITL review findings 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 --- .agents/autonomy.yaml | 3 + .agents/interaction-points.yaml | 2 +- .agents/operational/sky-host.agent.yaml | 6 +- .cursor/rules/sky-host.mdc | 18 ++- .cursor/rules/sky-intake.mdc | 5 +- .cursor/skills/sky-experience/SKILL.md | 2 +- .cursor/skills/sky-interact/SKILL.md | 18 +-- .skills/sky-interact.skill.yaml | 9 +- docs/_meta/USER_JOURNEY.md | 6 +- docs/schemas/interaction-turn.schema.yaml | 19 +++ scripts/agents/choreograph-agents.ps1 | 2 +- scripts/sky/assess-platform.ps1 | 21 +-- scripts/sky/attach-plugin.ps1 | 9 +- scripts/sky/interaction-catalog.ps1 | 184 +++++++++++++--------- scripts/sky/new-session.ps1 | 28 +--- 15 files changed, 199 insertions(+), 133 deletions(-) diff --git a/.agents/autonomy.yaml b/.agents/autonomy.yaml index 3c08ca9..d9839a8 100644 --- a/.agents/autonomy.yaml +++ b/.agents/autonomy.yaml @@ -50,6 +50,9 @@ human_gates: elevation: required_before: [public] approval_stage: elevation + architecture: + required_before: [invoke_skill, side_effect] + approval_stage: architecture package: required_before: [side_effect] approval_stage: package diff --git a/.agents/interaction-points.yaml b/.agents/interaction-points.yaml index aea62d7..337102c 100644 --- a/.agents/interaction-points.yaml +++ b/.agents/interaction-points.yaml @@ -132,7 +132,7 @@ points: phase: any agent: sky-host skill: sky-interact - when: Ação bloqueada por human gate (brief|architecture|package|public_showcase) + when: Ação bloqueada por human gate (brief|elevation|architecture|package|public_showcase) prompt: Há um gate humano pendente. Como seguir? options: - id: approve diff --git a/.agents/operational/sky-host.agent.yaml b/.agents/operational/sky-host.agent.yaml index 618dfdd..d437308 100644 --- a/.agents/operational/sky-host.agent.yaml +++ b/.agents/operational/sky-host.agent.yaml @@ -38,10 +38,8 @@ elicitation: channel_order: [ask_question, numbered_chat] decision_points_ref: .agents/interaction-points.yaml arrival_point: arrival.intent - arrival_prompts: - - Tem uma ideia nova ou quer retomar um projeto? - - Trouxe material existente (zip, docs, mockup)? - - Quer ver o que já foi feito visualmente? + # Uma decisão por turno — prompt/opções vêm do catálogo (arrival.intent) + arrival_prompts_ref: arrival.intent resume_template: | Bem-vindo de volta ao **{title}**. Maturidade: **{readiness}%** · Fase: **{phase_label}** diff --git a/.cursor/rules/sky-host.mdc b/.cursor/rules/sky-host.mdc index 2a98db2..ee881de 100644 --- a/.cursor/rules/sky-host.mdc +++ b/.cursor/rules/sky-host.mdc @@ -15,9 +15,13 @@ Você é o **anfitrião** da experiência. O usuário não deve adivinhar o pró ## Antes de cada resposta -1. Se há slug: ler `.sky/sessions/{slug}/journey.yaml`, `maturity.yaml` e `agentic-repo-recommendation.yaml` (se existir) +1. Se há slug, ler o contrato de sessão (quando existirem): + - `.sky/sessions/{slug}/journey.yaml` + - `maturity.yaml` (top gaps: business, product, ux_design, elevation, …) + - `sky-merits.yaml`, `brief-draft.yaml`, `alternatives.yaml`, `ux-spec.yaml` + - `agentic-repo-recommendation.yaml` (se existir) 2. Se `journey.yaml` tem `pending_interaction.status: pending` → **elicitar agora** (passo Interação) -3. Se não há slug: perguntar intenção (ponto `arrival.intent`) +3. Se não há slug: perguntar intenção (ponto `arrival.intent` do catálogo — **uma** pergunta) 4. [USER_JOURNEY.md](../../docs/_meta/USER_JOURNEY.md) · [SKY_INTERACT.md](../../docs/_meta/SKY_INTERACT.md) · skill `sky-interact` ## Interação HITL (obrigatório em decision points) @@ -29,11 +33,13 @@ Catálogo: [`.agents/interaction-points.yaml`](../../.agents/interaction-points. - Escape opcional: `Outra coisa (vou digitar)` - Não duplicar as opções em prosa no mesmo turno 2. **Fallback** se AskQuestion indisponível — formato abaixo -3. Após a escolha: resolver via +3. Após a escolha: **somente** via CLI auditado `./scripts/sky/sky.ps1 interact -Slug {slug} -Resolve -ChoiceId {id}` - (ou atualizar `pending_interaction` no journey) e executar **uma** ação / handoff + Isso grava `choice_id` / `next_suggested_actions` e `human.interaction.answered`. + Não editar `pending_interaction` manualmente. 4. CLI para gravar pending: - `./scripts/sky/sky.ps1 interact -Slug {slug} -PointId {point.id}` + `./scripts/sky/sky.ps1 interact -Slug {slug} -PointId {point.id}` + Depois executar **uma** ação / handoff. Pontos típicos: `arrival.intent`, `brownfield.after_attach`, `assess.next_action`, `intake.deepen_gap`, `elevate.confirm`, `gate.approve_stage`, `deliver.export_scope`, @@ -62,7 +68,7 @@ Quando AskQuestion for usado, o cabeçalho maturidade/fase pode ficar numa linha | Situação | Agente | |----------|--------| | Ideia nova, lacunas, brownfield | intake-conductor | -| Repo existente — plugin host | `sky attach` → intake-conductor + assess → **HITL** `assess.next_action` | +| Repo existente — plugin host | `sky attach` → HITL `brownfield.after_attach` (se sem assess) **ou** assess → HITL `assess.next_action` | | Export, pasta externa, pacote | delivery-steward | | Site visual, publicar preview | showcase-curator | | Índices SKY, elevação | sky-elevator + ux-design-specialist | diff --git a/.cursor/rules/sky-intake.mdc b/.cursor/rules/sky-intake.mdc index bdf3c37..e28346e 100644 --- a/.cursor/rules/sky-intake.mdc +++ b/.cursor/rules/sky-intake.mdc @@ -21,9 +21,10 @@ Conduza elicitação dinâmica e **eleve** a proposta quando `policies.open_to_e Quando houver lacunas e o usuário não escolheu qual preencher: -1. Opcional: `./scripts/sky/sky.ps1 interact -Slug {slug} -PointId intake.deepen_gap` +1. **Obrigatório** — persistir pending antes de perguntar: + `./scripts/sky/sky.ps1 interact -Slug {slug} -PointId intake.deepen_gap` 2. Preferir **AskQuestion** com as top lacunas (máx. 4) — ponto `intake.deepen_gap` -3. Só então atualizar artefatos da dimensão escolhida +3. Só após `-Resolve` atualizar artefatos da dimensão escolhida Brownfield pós-assess: respeitar `assess.next_action` / `pending_interaction` do journey. diff --git a/.cursor/skills/sky-experience/SKILL.md b/.cursor/skills/sky-experience/SKILL.md index 017c7ee..74f6594 100644 --- a/.cursor/skills/sky-experience/SKILL.md +++ b/.cursor/skills/sky-experience/SKILL.md @@ -20,7 +20,7 @@ Use quando o usuário interage com Sky-Forge sem saber o próximo passo, quer ex ## Antes de responder -1. `.sky/sessions/{slug}/journey.yaml` + `maturity.yaml` +1. Contrato de sessão (quando existirem): `journey.yaml`, `maturity.yaml`, `sky-merits.yaml`, `brief-draft.yaml`, `alternatives.yaml`, `ux-spec.yaml` 2. Se `pending_interaction.status: pending` → carregar skill **sky-interact** e elicitar (AskQuestion / fallback) 3. [USER_JOURNEY.md](../../../docs/_meta/USER_JOURNEY.md) · [SKY_INTERACT.md](../../../docs/_meta/SKY_INTERACT.md) diff --git a/.cursor/skills/sky-interact/SKILL.md b/.cursor/skills/sky-interact/SKILL.md index e812d39..02290e6 100644 --- a/.cursor/skills/sky-interact/SKILL.md +++ b/.cursor/skills/sky-interact/SKILL.md @@ -29,7 +29,7 @@ um agente estiver prestes a escolher sozinho entre caminhos divergentes. ## Protocolo (ordem) -1. **Ler contexto** — `journey.yaml`, `maturity.yaml`, e o `point` no catálogo. +1. **Ler contexto de sessão** (quando existirem): `journey.yaml`, `maturity.yaml` (top gaps incl. `elevation` / `ux_design`), `sky-merits.yaml`, `brief-draft.yaml`, `alternatives.yaml`, `ux-spec.yaml`, e o `point` no catálogo. 2. **Preferir AskQuestion** — se a ferramenta `AskQuestion` estiver disponível neste turno: - Uma pergunta por mensagem - Até **4** opções (labels curtos) @@ -37,14 +37,14 @@ um agente estiver prestes a escolher sozinho entre caminhos divergentes. - Não listar as mesmas opções em prosa no mesmo turno 3. **Fallback** — se AskQuestion indisponível (modelo/modo): - Formato sky-host: maturidade · fase · opções `1)…4)` · “Responda com o número…” -4. **Após a resposta**: - - Atualizar `journey.yaml`: - - `pending_interaction.status: resolved` - - `pending_interaction.choice_id` / `choice_label` - - `next_suggested_actions` coerentes com a escolha - - Registrar auditoria: `human.interaction.answered` - - Seguir o `routes_to` / comando do catálogo (**uma** ação) -5. **CLI auxiliar** (opcional, grava o pending no disco): +4. **Após a resposta** — resolver **somente** via CLI auditado (não editar YAML à mão): + +```powershell +./scripts/sky/sky.ps1 interact -Slug -Resolve -ChoiceId +``` + + Isso grava `choice_id` / `next_suggested_actions` e `human.interaction.answered`. Depois seguir **uma** ação (`routes_to` / `command`). +5. **CLI para abrir pending**: ```powershell ./scripts/sky/sky.ps1 interact -Slug -PointId diff --git a/.skills/sky-interact.skill.yaml b/.skills/sky-interact.skill.yaml index 0cd07f6..2fa016d 100644 --- a/.skills/sky-interact.skill.yaml +++ b/.skills/sky-interact.skill.yaml @@ -10,12 +10,19 @@ steps: - read: .agents/interaction-points.yaml - read: docs/_meta/SKY_INTERACT.md - read: .sky/sessions/{slug}/journey.yaml + - read: .sky/sessions/{slug}/maturity.yaml + - read: .sky/sessions/{slug}/sky-merits.yaml + - read: .sky/sessions/{slug}/brief-draft.yaml + - read: .sky/sessions/{slug}/alternatives.yaml + - read: .sky/sessions/{slug}/ux-spec.yaml - script: scripts/sky/prompt-interaction.ps1 -Slug {slug} -PointId {point_id} - agent: > Pausar. Preferir ferramenta AskQuestion com prompt/options do ponto. Se indisponível, opções numeradas (sky-host). Após escolha, resolver - pending_interaction e rotear. + SOMENTE via: sky interact -Resolve -ChoiceId {id} (auditoria + human.interaction.answered). Uma ação de follow-up apenas. guardrails: one_question_per_turn: true max_options: 4 + resolve_via_cli_only: true never_stack_with: [export_and_publish, multiple_human_gates] diff --git a/docs/_meta/USER_JOURNEY.md b/docs/_meta/USER_JOURNEY.md index 38f7df9..8602787 100644 --- a/docs/_meta/USER_JOURNEY.md +++ b/docs/_meta/USER_JOURNEY.md @@ -67,10 +67,14 @@ Após `attach` / `assess` / lacunas / gates, o agente **pausa** e elicita: | Gatilho | PointId | |---------|---------| | Chegada | `arrival.intent` | -| Pós-attach | `brownfield.after_attach` | +| Pós-attach (sem assess) | `brownfield.after_attach` | | Pós-assess | `assess.next_action` | | Lacunas | `intake.deepen_gap` | +| Elevação | `elevate.confirm` | +| Gate humano | `gate.approve_stage` | +| Escopo de export | `deliver.export_scope` | | Antes de `-Public` | `showcase.privacy` | +| ARAH Harness | `implement.agentic_repo` | ## Handoffs entre agentes diff --git a/docs/schemas/interaction-turn.schema.yaml b/docs/schemas/interaction-turn.schema.yaml index 949279d..59855e7 100644 --- a/docs/schemas/interaction-turn.schema.yaml +++ b/docs/schemas/interaction-turn.schema.yaml @@ -53,3 +53,22 @@ properties: freeform: type: [string, 'null'] description: Texto livre quando o usuário escolheu escape freeform +allOf: + - if: + properties: + status: + const: resolved + required: [status] + then: + required: [resolved_at] + anyOf: + - required: [choice_id] + properties: + choice_id: + type: string + minLength: 1 + - required: [freeform] + properties: + freeform: + type: string + minLength: 1 diff --git a/scripts/agents/choreograph-agents.ps1 b/scripts/agents/choreograph-agents.ps1 index fc4a3a8..a36e47f 100644 --- a/scripts/agents/choreograph-agents.ps1 +++ b/scripts/agents/choreograph-agents.ps1 @@ -10,7 +10,7 @@ param( [string[]]$ChangedFiles = @(), [string]$Slug = '', [string]$Intent = '', - [ValidateSet('manual', 'path_change', 'maturity_gate', 'post_export', 'hook_stop')] + [ValidateSet('manual', 'path_change', 'maturity_gate', 'post_export', 'post_assess', 'post_attach', 'hook_stop')] [string]$Trigger = 'manual', [string]$Action = '', [string]$AgentId = '', diff --git a/scripts/sky/assess-platform.ps1 b/scripts/sky/assess-platform.ps1 index 7f861a0..335faf3 100644 --- a/scripts/sky/assess-platform.ps1 +++ b/scripts/sky/assess-platform.ps1 @@ -194,14 +194,15 @@ if ($gaps.Count -gt 0) { # HITL: gravar pending_interaction para o agente Cursor (AskQuestion / fallback) $interactScript = Join-Path $PSScriptRoot 'prompt-interaction.ps1' -if (Test-Path $interactScript) { - $topGap = if ($gaps.Count -gt 0) { $gaps[0] } else { $null } - $promptOverride = if ($topGap) { - "Assessment pronto (principal lacuna: $topGap). O que prefere agora?" - } else { - 'Assessment pronto. O que prefere agora?' - } - Write-Host "" - Write-Host "=== Proximo passo (interacao HITL) ===" -ForegroundColor Cyan - & $interactScript -Slug $Slug -PointId 'assess.next_action' -Prompt $promptOverride -ErrorAction SilentlyContinue +if (-not (Test-Path $interactScript)) { + throw "HITL obrigatorio apos assess: script ausente ($interactScript)" +} +$topGap = if ($gaps.Count -gt 0) { $gaps[0] } else { $null } +$promptOverride = if ($topGap) { + "Assessment pronto (principal lacuna: $topGap). O que prefere agora?" +} else { + 'Assessment pronto. O que prefere agora?' } +Write-Host "" +Write-Host "=== Proximo passo (interacao HITL) ===" -ForegroundColor Cyan +& $interactScript -Slug $Slug -PointId 'assess.next_action' -Prompt $promptOverride diff --git a/scripts/sky/attach-plugin.ps1 b/scripts/sky/attach-plugin.ps1 index 3f7132d..91ae4bc 100644 --- a/scripts/sky/attach-plugin.ps1 +++ b/scripts/sky/attach-plugin.ps1 @@ -173,11 +173,12 @@ if ($runAssess) { & (Join-Path $PSScriptRoot 'assess-platform.ps1') -Slug $Slug -WorkspacePath $workspace } else { $interactScript = Join-Path $PSScriptRoot 'prompt-interaction.ps1' - if (Test-Path $interactScript) { - Write-Host "" - Write-Host "=== Proximo passo (interacao HITL) ===" -ForegroundColor Cyan - & $interactScript -Slug $Slug -PointId 'brownfield.after_attach' -ErrorAction SilentlyContinue + if (-not (Test-Path $interactScript)) { + throw "HITL obrigatorio apos attach: script ausente ($interactScript)" } + Write-Host "" + Write-Host "=== Proximo passo (interacao HITL) ===" -ForegroundColor Cyan + & $interactScript -Slug $Slug -PointId 'brownfield.after_attach' } Write-Host "" diff --git a/scripts/sky/interaction-catalog.ps1 b/scripts/sky/interaction-catalog.ps1 index 6b32ae8..977e6f6 100644 --- a/scripts/sky/interaction-catalog.ps1 +++ b/scripts/sky/interaction-catalog.ps1 @@ -1,11 +1,95 @@ #Requires -Version 5.1 <# .SYNOPSIS - Catálogo embutido de pontos de interação (espelho de .agents/interaction-points.yaml). + Catálogo de pontos de interação — fonte: .agents/interaction-points.yaml Dot-source: . (Join-Path $PSScriptRoot 'interaction-catalog.ps1') #> -function Get-SkyInteractionCatalog { +function Escape-SkyYamlDoubleQuoted { + param([AllowNull()][string]$Text) + if ($null -eq $Text) { return '' } + $t = $Text -replace '\\', '\\' + $t = $t -replace '"', '\"' + $t = $t -replace "`r`n", '\n' + $t = $t -replace "`n", '\n' + $t = $t -replace "`r", '\n' + return $t +} + +function ConvertFrom-SkyInteractionPointsYaml { + param([Parameter(Mandatory = $true)][string]$Path) + if (-not (Test-Path $Path)) { return $null } + $lines = @(Get-Content -Path $Path -Encoding UTF8) + $catalog = @{} + $inPoints = $false + $pointId = $null + $prompt = $null + $optList = @() + $inOptions = $false + $curOpt = $null + + foreach ($line in $lines) { + if ($line -match '^\s*#') { continue } + if (-not $inPoints) { + if ($line -match '^points\s*:') { $inPoints = $true } + continue + } + if ($line -match '^ - id:\s*(\S+)\s*$') { + if ($pointId) { + if ($null -ne $curOpt) { $optList += $curOpt; $curOpt = $null } + $catalog[$pointId] = @{ prompt = $(if ($prompt) { $prompt } else { '' }); options = @($optList) } + } + $pointId = $Matches[1] + $prompt = $null + $optList = @() + $inOptions = $false + $curOpt = $null + continue + } + if (-not $pointId) { continue } + if ($line -match '^\s+prompt:\s*(.+)\s*$') { + $prompt = $Matches[1].Trim().Trim('"').Trim("'") + continue + } + if ($line -match '^\s+options(_fallback)?\s*:') { + if ($null -ne $curOpt) { $optList += $curOpt; $curOpt = $null } + $inOptions = $true + continue + } + if ($inOptions -and $line -match '^\s+- id:\s*(\S+)\s*$') { + if ($null -ne $curOpt) { $optList += $curOpt } + $curOpt = @{ id = $Matches[1]; label = $Matches[1] } + continue + } + if ($inOptions -and $null -ne $curOpt) { + if ($line -match '^\s+label:\s*(.+)\s*$') { + $curOpt.label = $Matches[1].Trim().Trim('"').Trim("'") + continue + } + if ($line -match '^\s+routes_to:\s*(\S+)\s*$') { + $curOpt.routes_to = $Matches[1] + continue + } + if ($line -match '^\s+command:\s*(.+)\s*$') { + $curOpt.command = $Matches[1].Trim().Trim('"').Trim("'") + continue + } + } + if ($inOptions -and $line -match '^ [a-z_]' -and $line -notmatch '^\s+- ' -and $line -notmatch '^\s+(label|routes_to|command|sets|requires_gate):') { + if ($null -ne $curOpt) { $optList += $curOpt; $curOpt = $null } + $inOptions = $false + } + } + if ($pointId) { + if ($null -ne $curOpt) { $optList += $curOpt } + $catalog[$pointId] = @{ prompt = $(if ($prompt) { $prompt } else { '' }); options = @($optList) } + } + if ($catalog.Count -eq 0) { return $null } + return $catalog +} + +function Get-SkyInteractionCatalogFallback { + # Usado só se o YAML estiver ausente/ilegível return @{ 'arrival.intent' = @{ prompt = 'O que você quer fazer no Sky-Forge agora?' @@ -16,23 +100,6 @@ function Get-SkyInteractionCatalog { @{ id = 'status_only'; label = 'Só ver status de um projeto'; routes_to = 'sky-host' } ) } - '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' } - ) - } - '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' } - ) - } 'assess.next_action' = @{ prompt = 'Assessment pronto. O que prefere agora?' options = @( @@ -42,57 +109,31 @@ function Get-SkyInteractionCatalog { @{ id = 'status'; label = 'Só revisar o status'; command = './scripts/sky/sky.ps1 status -Slug {slug}' } ) } - '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)' } - ) - } - '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' } - ) - } - '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' } - ) - } - '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' } - ) - } - 'showcase.privacy' = @{ - prompt = 'Sobre privacidade do preview público…' + 'brownfield.after_attach' = @{ + prompt = 'Host plugin anexado. Próximo passo?' 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' } + @{ 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' } ) } - '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' } - ) + } +} + +function Get-SkyInteractionCatalog { + $repoRoot = $null + if ($PSScriptRoot) { + $repoRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..') -ErrorAction SilentlyContinue + } + if ($repoRoot) { + $yamlPath = Join-Path $repoRoot.Path '.agents\interaction-points.yaml' + $fromYaml = ConvertFrom-SkyInteractionPointsYaml -Path $yamlPath + if ($fromYaml -and $fromYaml.Count -gt 0) { + return $fromYaml } } + return Get-SkyInteractionCatalogFallback } function Get-SkyPendingInteractionSnapshot { @@ -144,13 +185,13 @@ function Format-SkyOptionNextActionLines { ) $lines = @() $lines += " - id: $($Option.id)" - $lbl = ($Option.label -replace '"', '''') + $lbl = Escape-SkyYamlDoubleQuoted $Option.label $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 '"', '''')`"" + $lines += " command: `"$(Escape-SkyYamlDoubleQuoted $cmd)`"" } return $lines } @@ -187,23 +228,24 @@ function Format-SkyPendingInteractionYaml { [void]$sb.AppendLine(' choice_id: null') } if ($ChoiceLabel) { - $safe = $ChoiceLabel -replace '"', '''' + $safe = Escape-SkyYamlDoubleQuoted $ChoiceLabel [void]$sb.AppendLine(" choice_label: `"$safe`"") } else { [void]$sb.AppendLine(' choice_label: null') } - $promptSafe = ($Prompt -replace '"', '''') + $promptSafe = Escape-SkyYamlDoubleQuoted $Prompt [void]$sb.AppendLine(" prompt: `"$promptSafe`"") [void]$sb.AppendLine(' options:') foreach ($opt in $Options) { [void]$sb.AppendLine(" - id: $($opt.id)") - $lbl = ($opt.label -replace '"', '''') + $lbl = Escape-SkyYamlDoubleQuoted $opt.label [void]$sb.AppendLine(" label: `"$lbl`"") if ($opt.routes_to) { [void]$sb.AppendLine(" routes_to: $($opt.routes_to)") } if ($opt.command) { - [void]$sb.AppendLine(" command: `"$($opt.command)`"") + $cmd = Escape-SkyYamlDoubleQuoted $opt.command + [void]$sb.AppendLine(" command: `"$cmd`"") } } return $sb.ToString().TrimEnd() diff --git a/scripts/sky/new-session.ps1 b/scripts/sky/new-session.ps1 index 736e14f..0f539d2 100644 --- a/scripts/sky/new-session.ps1 +++ b/scripts/sky/new-session.ps1 @@ -146,6 +146,11 @@ pending_suggestions: [] "@ } if ($f -eq 'journey.yaml') { + . (Join-Path $PSScriptRoot 'interaction-catalog.ps1') + $arrival = (Get-SkyInteractionCatalog)['arrival.intent'] + if (-not $arrival) { throw 'Catalogo sem arrival.intent — verifique .agents/interaction-points.yaml' } + $pendingBlock = Format-SkyPendingInteractionYaml -PointId 'arrival.intent' -Prompt $arrival.prompt ` + -Options $arrival.options -Status 'pending' -Channel 'ask_question' -CreatedAt $now $content = @" version: "1.0" slug: $Slug @@ -176,28 +181,7 @@ next_suggested_actions: notes: | Criado pelo sky-intake. sky-host conduz via sky-interact (AskQuestion ou fallback numerado). -pending_interaction: - id: arrival.intent - status: pending - channel: ask_question - created_at: "$now" - resolved_at: null - choice_id: null - choice_label: null - 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 +$pendingBlock "@ } Set-Content -Path $dest -Value $content -Encoding UTF8 -NoNewline From 617077b8506d34de17e0a242f26b90d621fc7283 Mon Sep 17 00:00:00 2001 From: Raphael Date: Wed, 29 Jul 2026 00:10:22 -0300 Subject: [PATCH 4/6] fix(interact): expand command placeholders and harden attach HITL 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 --- scripts/sky/attach-plugin.ps1 | 14 ++- scripts/sky/interaction-catalog.ps1 | 186 +++++++++++++++++++++++++++- scripts/sky/prompt-interaction.ps1 | 79 ++++++++++-- scripts/sky/sky.ps1 | 10 ++ 4 files changed, 270 insertions(+), 19 deletions(-) diff --git a/scripts/sky/attach-plugin.ps1 b/scripts/sky/attach-plugin.ps1 index 91ae4bc..94d9b9d 100644 --- a/scripts/sky/attach-plugin.ps1 +++ b/scripts/sky/attach-plugin.ps1 @@ -83,10 +83,6 @@ if (Test-Path $journeyPath) { $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' @@ -168,6 +164,14 @@ if (Test-Path $linkFile) { } $runAssess = $Assess -or -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. +. (Join-Path $PSScriptRoot 'interaction-catalog.ps1') +if (Test-Path $journeyPath) { + Clear-SkyJourneyPendingInteraction -JourneyPath $journeyPath +} + if ($runAssess) { Write-Host "" & (Join-Path $PSScriptRoot 'assess-platform.ps1') -Slug $Slug -WorkspacePath $workspace @@ -178,7 +182,7 @@ if ($runAssess) { } 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 index 977e6f6..97d1b27 100644 --- a/scripts/sky/interaction-catalog.ps1 +++ b/scripts/sky/interaction-catalog.ps1 @@ -16,6 +16,15 @@ function Escape-SkyYamlDoubleQuoted { return $t } +function Unescape-SkyYamlDoubleQuoted { + param([AllowNull()][string]$Text) + if ($null -eq $Text) { return '' } + $t = $Text -replace '\\n', "`n" + $t = $t -replace '\\"', '"' + $t = $t -replace '\\\\', '\' + return $t +} + function ConvertFrom-SkyInteractionPointsYaml { param([Parameter(Mandatory = $true)][string]$Path) if (-not (Test-Path $Path)) { return $null } @@ -74,6 +83,14 @@ function ConvertFrom-SkyInteractionPointsYaml { $curOpt.command = $Matches[1].Trim().Trim('"').Trim("'") continue } + if ($line -match '^\s+sets:\s*(.+)\s*$') { + $curOpt.sets = $Matches[1].Trim().Trim('"').Trim("'") + continue + } + if ($line -match '^\s+requires_gate:\s*(\S+)\s*$') { + $curOpt.requires_gate = $Matches[1] + continue + } } if ($inOptions -and $line -match '^ [a-z_]' -and $line -notmatch '^\s+- ' -and $line -notmatch '^\s+(label|routes_to|command|sets|requires_gate):') { if ($null -ne $curOpt) { $optList += $curOpt; $curOpt = $null } @@ -150,6 +167,7 @@ function Get-SkyPendingInteractionSnapshot { created_at = $null channel = $null option_ids = @() + options = @() } if ($JourneyRaw -notmatch '(?m)^pending_interaction\s*:') { return $snap } @@ -169,19 +187,172 @@ function Get-SkyPendingInteractionSnapshot { $snap.created_at = $Matches[1].Trim().Trim('"') } if ($pendingChunk -match '(?m)^ prompt:\s*"((?:\\.|[^"])*)"') { - $snap.prompt = $Matches[1] -replace "''", "'" + $snap.prompt = Unescape-SkyYamlDoubleQuoted $Matches[1] } elseif ($pendingChunk -match '(?m)^ prompt:\s*(.+)$') { $snap.prompt = $Matches[1].Trim().Trim('"') } - $ids = [regex]::Matches($pendingChunk, '(?m)^ - id:\s*(\S+)') - $snap.option_ids = @($ids | ForEach-Object { $_.Groups[1].Value }) + + $optList = @() + $cur = $null + foreach ($line in ($pendingChunk -split '\r?\n')) { + if ($line -match '^ - id:\s*(\S+)') { + if ($null -ne $cur) { $optList += $cur } + $cur = @{ id = $Matches[1]; label = $Matches[1] } + continue + } + if ($null -eq $cur) { continue } + if ($line -match '^ label:\s*"((?:\\.|[^"])*)"') { + $cur.label = Unescape-SkyYamlDoubleQuoted $Matches[1] + continue + } + if ($line -match '^ label:\s*(.+)$') { + $cur.label = $Matches[1].Trim().Trim('"') + continue + } + if ($line -match '^ routes_to:\s*(\S+)') { $cur.routes_to = $Matches[1]; continue } + if ($line -match '^ command:\s*"((?:\\.|[^"])*)"') { + $cur.command = Unescape-SkyYamlDoubleQuoted $Matches[1] + continue + } + if ($line -match '^ command:\s*(.+)$') { + $cur.command = $Matches[1].Trim().Trim('"') + continue + } + if ($line -match '^ sets:\s*"((?:\\.|[^"])*)"') { + $cur.sets = Unescape-SkyYamlDoubleQuoted $Matches[1] + continue + } + if ($line -match '^ sets:\s*(.+)$') { + $cur.sets = $Matches[1].Trim().Trim('"') + continue + } + } + if ($null -ne $cur) { $optList += $cur } + $snap.options = @($optList) + $snap.option_ids = @($optList | ForEach-Object { $_.id }) return $snap } +function Expand-SkyCommandPlaceholders { + param( + [string]$Command, + [string]$Slug = '', + [string]$Workspace = '', + [string]$Stage = '' + ) + if ([string]::IsNullOrWhiteSpace($Command)) { return $Command } + $cmd = $Command + if ($Slug) { $cmd = $cmd.Replace('{slug}', $Slug) } + if ($Workspace) { $cmd = $cmd.Replace('{workspace}', $Workspace) } + if ($Stage) { $cmd = $cmd.Replace('{stage}', $Stage) } + return $cmd +} + +function Resolve-SkyInteractWorkspacePath { + param( + [Parameter(Mandatory = $true)][string]$RepoRoot, + [Parameter(Mandatory = $true)][string]$Slug, + [string]$Explicit = '' + ) + if ($Explicit) { return $Explicit } + $gitPath = Join-Path $RepoRoot ".sky\sessions\$Slug\git.yaml" + if (Test-Path $gitPath) { + $raw = Get-Content $gitPath -Raw + if ($raw -match '(?m)^workspace_path:\s*(.+)$') { + $p = $Matches[1].Trim().Trim('"').Trim("'") + if ($p -and $p -ne 'null') { return $p } + } + } + return '' +} + +function Resolve-SkyInteractStage { + param( + [string]$SessionDir, + [string]$Explicit = '' + ) + if ($Explicit) { return $Explicit } + $approvals = Join-Path $SessionDir 'approvals.yaml' + if (-not (Test-Path $approvals)) { + return 'brief' + } + $raw = Get-Content $approvals -Raw + foreach ($s in @('brief', 'elevation', 'architecture', 'package', 'public_showcase')) { + if ($raw -notmatch "(?m)^\s*${s}\s*:") { + return $s + } + } + return 'package' +} + +function Get-SkyMaturityTopGapOptions { + param([Parameter(Mandatory = $true)][string]$MaturityPath) + if (-not (Test-Path $MaturityPath)) { return @() } + $raw = Get-Content $MaturityPath -Raw + $dimOrder = @('business', 'product', 'ux_design', 'technical', 'sustainability', 'elevation') + $labels = @{ + business = 'Negócio' + product = 'Produto' + ux_design = 'UX' + technical = 'Técnico' + sustainability = 'Sustentação' + elevation = 'Elevação' + } + $scored = @() + foreach ($dim in $dimOrder) { + $score = 1.0 + if ($raw -match "(?ms)^\s+$dim`:\s*\r?\n(?:.*?\r?\n)*?\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*\[([^\]]*)\]") { + $inner = $Matches[1] + if ($inner -match '"([^"]+)"') { $gap = $Matches[1] } + elseif ($inner -match "'([^']+)'") { $gap = $Matches[1] } + } + if (-not $gap) { continue } + $scored += [PSCustomObject]@{ id = $dim; score = $score; gap = $gap; title = $labels[$dim] } + } + $top = @($scored | Sort-Object score | Select-Object -First 4) + $opts = @() + foreach ($t in $top) { + $opts += @{ + id = $t.id + label = "$($t.title): $($t.gap)" + } + } + return $opts +} + +function Apply-SkyOptionSets { + param( + [Parameter(Mandatory = $true)][string]$SessionDir, + [string]$Sets + ) + if ([string]::IsNullOrWhiteSpace($Sets)) { return } + # policies.open_to_elevation=false + if ($Sets -match '^policies\.([A-Za-z0-9_]+)=(.+)$') { + $key = $Matches[1] + $val = $Matches[2].Trim() + $merits = Join-Path $SessionDir 'sky-merits.yaml' + if (-not (Test-Path $merits)) { return } + $raw = Get-Content $merits -Raw + if ($raw -match "(?m)^(\s+)$([regex]::Escape($key)):\s*.+$") { + $raw = [regex]::Replace($raw, "(?m)^(\s+)$([regex]::Escape($key)):\s*.+$", "`${1}${key}: $val") + } elseif ($raw -match '(?m)^policies\s*:') { + $raw = [regex]::Replace($raw, '(?m)^(policies\s*:)', "`$1`r`n ${key}: $val") + } + Set-Content -Path $merits -Value $raw -Encoding UTF8 + Write-Host "OK: policies.$key = $val (sky-merits.yaml)" -ForegroundColor DarkCyan + } +} + function Format-SkyOptionNextActionLines { param( [Parameter(Mandatory = $true)]$Option, - [string]$Slug = '' + [string]$Slug = '', + [string]$Workspace = '', + [string]$Stage = '' ) $lines = @() $lines += " - id: $($Option.id)" @@ -189,8 +360,7 @@ function Format-SkyOptionNextActionLines { $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 } + $cmd = Expand-SkyCommandPlaceholders -Command $Option.command -Slug $Slug -Workspace $Workspace -Stage $Stage $lines += " command: `"$(Escape-SkyYamlDoubleQuoted $cmd)`"" } return $lines @@ -247,6 +417,10 @@ function Format-SkyPendingInteractionYaml { $cmd = Escape-SkyYamlDoubleQuoted $opt.command [void]$sb.AppendLine(" command: `"$cmd`"") } + if ($opt.sets) { + $sets = Escape-SkyYamlDoubleQuoted $opt.sets + [void]$sb.AppendLine(" sets: `"$sets`"") + } } return $sb.ToString().TrimEnd() } diff --git a/scripts/sky/prompt-interaction.ps1 b/scripts/sky/prompt-interaction.ps1 index f2f7c57..4459e72 100644 --- a/scripts/sky/prompt-interaction.ps1 +++ b/scripts/sky/prompt-interaction.ps1 @@ -32,6 +32,12 @@ param( [Parameter()] [string]$Channel = 'ask_question', + [Parameter()] + [string]$WorkspacePath = '', + + [Parameter()] + [string]$Stage = '', + [Parameter()] [switch]$NoAudit ) @@ -47,6 +53,8 @@ if (-not (Test-Path $journeyPath)) { } $rec = Join-Path $PSScriptRoot 'record-agent-event.ps1' +$workspace = Resolve-SkyInteractWorkspacePath -RepoRoot $RepoRoot.Path -Slug $Slug -Explicit $WorkspacePath +$stageVal = Resolve-SkyInteractStage -SessionDir $sessionDir -Explicit $Stage if ($Clear) { Clear-SkyJourneyPendingInteraction -JourneyPath $journeyPath @@ -59,15 +67,37 @@ if ($Clear) { $catalog = Get-SkyInteractionCatalog +function Expand-OptionCommands { + param($Options) + $out = @() + foreach ($o in @($Options)) { + $copy = @{ + id = $o.id + label = $o.label + } + if ($o.routes_to) { $copy.routes_to = $o.routes_to } + if ($o.sets) { $copy.sets = $o.sets } + if ($o.requires_gate) { $copy.requires_gate = $o.requires_gate } + if ($o.command) { + $copy.command = Expand-SkyCommandPlaceholders -Command $o.command -Slug $Slug -Workspace $workspace -Stage $stageVal + } + $out += $copy + } + return $out +} + if ($Resolve) { if (-not $ChoiceId) { throw 'Resolve requer -ChoiceId' } $raw = Get-Content $journeyPath -Raw $snap = Get-SkyPendingInteractionSnapshot -JourneyRaw $raw $pointKey = if ($PointId) { $PointId } elseif ($snap.id) { $snap.id } else { 'unknown' } + # Preferir opções exatamente como elicitadas (disco); catalogo só completa metadata $opts = @() if ($OptionsJson) { $opts = @($OptionsJson | ConvertFrom-Json) + } elseif ($snap.options -and $snap.options.Count -gt 0) { + $opts = @($snap.options) } elseif ($catalog.ContainsKey($pointKey)) { $opts = @($catalog[$pointKey].options) } @@ -87,30 +117,46 @@ if ($Resolve) { throw "ChoiceId invalido: '$ChoiceId' para o ponto '$pointKey'. Opcoes: $known" } + # Completar command/sets/routes do catalogo se o pending nao tinha + if ($catalog.ContainsKey($pointKey)) { + $catOpt = $catalog[$pointKey].options | Where-Object { $_.id -eq $ChoiceId } | Select-Object -First 1 + if ($catOpt) { + if (-not $match.command -and $catOpt.command) { $match.command = $catOpt.command } + if (-not $match.routes_to -and $catOpt.routes_to) { $match.routes_to = $catOpt.routes_to } + if (-not $match.sets -and $catOpt.sets) { $match.sets = $catOpt.sets } + if (-not $match.label -or $match.label -eq $match.id) { $match.label = $catOpt.label } + } + } + $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.' } $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) - } + $optsForBlock = Expand-OptionCommands -Options $( + if ($catalog.ContainsKey($pointKey)) { $catalog[$pointKey].options } else { $opts } + ) - $block = Format-SkyPendingInteractionYaml -PointId $pointKey -Prompt $resolvedPrompt -Options $opts ` + $block = Format-SkyPendingInteractionYaml -PointId $pointKey -Prompt $resolvedPrompt -Options $optsForBlock ` -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 -Workspace $workspace -Stage $stageVal Set-SkyJourneyPendingInteraction -JourneyPath $journeyPath -PendingYamlBlock $block ` -NextActionsYamlLines $nextLines -ReplaceNextActions + if ($match.sets) { + Apply-SkyOptionSets -SessionDir $sessionDir -Sets $match.sets + } + 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 } + $expandedCmd = if ($match.command) { + Expand-SkyCommandPlaceholders -Command $match.command -Slug $Slug -Workspace $workspace -Stage $stageVal + } else { $null } Write-Host "OK: interacao resolvida ($pointKey -> $ChoiceId)" -ForegroundColor Green $payload = [ordered]@{ slug = $Slug @@ -121,7 +167,8 @@ if ($Resolve) { 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 = $expandedCmd + sets = $(if ($match.sets) { $match.sets } else { $null }) agent_instruction = 'Seguir routes_to/comando do option escolhido; uma acao apenas.' } $payload | ConvertTo-Json -Depth 5 @@ -140,6 +187,19 @@ if ($PointId) { $entry = $catalog[$PointId] if (-not $Prompt) { $Prompt = $entry.prompt } $opts = $entry.options + + # Dynamic options from maturity gaps + if ($PointId -eq 'intake.deepen_gap') { + $maturityPath = Join-Path $sessionDir 'maturity.yaml' + $dyn = Get-SkyMaturityTopGapOptions -MaturityPath $maturityPath + if ($dyn -and $dyn.Count -gt 0) { + $opts = $dyn + if ($dyn.Count -lt 4) { + $escape = $entry.options | Where-Object { $_.id -eq 'something_else' } | Select-Object -First 1 + if ($escape) { $opts += $escape } + } + } + } } if ($OptionsJson) { $opts = @($OptionsJson | ConvertFrom-Json) @@ -148,6 +208,8 @@ if (-not $opts -or $opts.Count -eq 0) { throw 'Nenhuma opcao para a interacao' } +$opts = Expand-OptionCommands -Options $opts + $now = (Get-Date).ToUniversalTime().ToString('o') $block = Format-SkyPendingInteractionYaml -PointId $(if ($PointId) { $PointId } else { 'custom' }) ` -Prompt $Prompt -Options $opts -Status 'pending' -Channel $Channel -CreatedAt $now @@ -172,6 +234,7 @@ $askPayload = [ordered]@{ $o = [ordered]@{ id = $_.id; label = $_.label } if ($_.routes_to) { $o.routes_to = $_.routes_to } if ($_.command) { $o.command = $_.command } + if ($_.sets) { $o.sets = $_.sets } $o }) fallback = [ordered]@{ diff --git a/scripts/sky/sky.ps1 b/scripts/sky/sky.ps1 index 433b8cc..3ced38b 100644 --- a/scripts/sky/sky.ps1 +++ b/scripts/sky/sky.ps1 @@ -46,6 +46,12 @@ param( [Parameter()] [switch]$Force, + [Parameter()] + [switch]$Assess, + + [Parameter()] + [switch]$NoAssess, + [Parameter()] [switch]$Public, @@ -359,6 +365,8 @@ switch ($Command) { if ($WorkspacePath) { $attachArgs.WorkspacePath = $WorkspacePath } if ($SyncMode) { $attachArgs.SyncMode = $SyncMode } if ($Force) { $attachArgs.Force = $true } + if ($Assess) { $attachArgs.Assess = $true } + if ($NoAssess) { $attachArgs.NoAssess = $true } & (Join-Path $PSScriptRoot 'attach-plugin.ps1') @attachArgs if ($Slug) { Invoke-AgentAudit $Slug 'repo-scaffolder' 'workspace.attach_host_plugin' 'side_effect' 'ok' @@ -382,6 +390,8 @@ switch ($Command) { '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 From 3ffd7e66ab5ff2ef5386d1e9d0d3f28fdedd29f7 Mon Sep 17 00:00:00 2001 From: Raphael Date: Wed, 29 Jul 2026 01:01:10 -0300 Subject: [PATCH 5/6] fix(interact): harden stage resolution, YAML escapes, and attach HITL 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 --- .agents/interaction-points.yaml | 2 +- docs/_meta/AGENT_AUTONOMY.md | 3 +- docs/_meta/SKY_INTERACT.md | 2 +- docs/schemas/interaction-turn.schema.yaml | 12 ++ scripts/agents/choreograph-agents.ps1 | 2 +- scripts/sky/assess-platform.ps1 | 12 +- scripts/sky/attach-plugin.ps1 | 25 ++-- scripts/sky/check-autonomy.ps1 | 29 +++-- scripts/sky/interaction-catalog.ps1 | 121 ++++++++++++------ scripts/sky/new-session.ps1 | 16 +-- scripts/sky/prompt-interaction.ps1 | 6 +- scripts/sky/sky.ps1 | 23 +++- templates/sessions/example-horta/journey.yaml | 2 +- 13 files changed, 165 insertions(+), 90 deletions(-) diff --git a/.agents/interaction-points.yaml b/.agents/interaction-points.yaml index 337102c..e831bda 100644 --- a/.agents/interaction-points.yaml +++ b/.agents/interaction-points.yaml @@ -106,7 +106,7 @@ points: 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/docs/_meta/AGENT_AUTONOMY.md b/docs/_meta/AGENT_AUTONOMY.md index 0921a3f..3186424 100644 --- a/docs/_meta/AGENT_AUTONOMY.md +++ b/docs/_meta/AGENT_AUTONOMY.md @@ -20,8 +20,9 @@ Fonte: [`.agents/autonomy.yaml`](../../.agents/autonomy.yaml) | Gate | Bloqueia até aprovar | |------|----------------------| -| `brief` | Export e side effects | +| `brief` | Export e side effects / skill.invoke (actionGates) | | `elevation` | Publish público (se aplicável) | +| `architecture` | Skills/side effects de agentes com `requires_gate: [architecture]` (+ export) | | `package` | Export completo | | `public_showcase` | `-Public` no showcase | diff --git a/docs/_meta/SKY_INTERACT.md b/docs/_meta/SKY_INTERACT.md index 8107efc..89bb18c 100644 --- a/docs/_meta/SKY_INTERACT.md +++ b/docs/_meta/SKY_INTERACT.md @@ -71,7 +71,7 @@ Agente detecta decision_point | Fluxo | PointId gravado | |-------|-----------------| -| `sky attach` (sucesso) | `brownfield.after_attach` | +| `sky attach` (sucesso) | `brownfield.after_attach` (default; use `-Assess` para rodar assessment na hora) | | `sky assess` (sucesso) | `assess.next_action` | | `sky new-session` / intake | `arrival.intent` (em `next_suggested_actions` + pending opcional) | diff --git a/docs/schemas/interaction-turn.schema.yaml b/docs/schemas/interaction-turn.schema.yaml index 59855e7..d492cf1 100644 --- a/docs/schemas/interaction-turn.schema.yaml +++ b/docs/schemas/interaction-turn.schema.yaml @@ -36,6 +36,13 @@ properties: type: string command: type: string + sets: + type: string + description: > + Diretiva de sessão do catálogo (ex. policies.open_to_elevation=false), + persistida para Apply-SkyOptionSets no -Resolve. + requires_gate: + type: string channel: type: string enum: [ask_question, numbered_chat, cli] @@ -61,6 +68,11 @@ allOf: required: [status] then: required: [resolved_at] + properties: + resolved_at: + type: string + format: date-time + minLength: 1 anyOf: - required: [choice_id] properties: diff --git a/scripts/agents/choreograph-agents.ps1 b/scripts/agents/choreograph-agents.ps1 index a36e47f..8fa2b86 100644 --- a/scripts/agents/choreograph-agents.ps1 +++ b/scripts/agents/choreograph-agents.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 5.1 +#Requires -Version 5.1 <# .SYNOPSIS Resolve coreografia Sky-Forge: agentes, autonomia, skills e gates. diff --git a/scripts/sky/assess-platform.ps1 b/scripts/sky/assess-platform.ps1 index 335faf3..5c0ce98 100644 --- a/scripts/sky/assess-platform.ps1 +++ b/scripts/sky/assess-platform.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 5.1 +#Requires -Version 5.1 <# .SYNOPSIS Varre repo brownfield e gera platform-assessment.yaml + atualiza maturidade inicial. @@ -192,10 +192,10 @@ if ($gaps.Count -gt 0) { $gaps | ForEach-Object { Write-Host " - $_" -ForegroundColor DarkYellow } } -# HITL: gravar pending_interaction para o agente Cursor (AskQuestion / fallback) -$interactScript = Join-Path $PSScriptRoot 'prompt-interaction.ps1' -if (-not (Test-Path $interactScript)) { - throw "HITL obrigatorio apos assess: script ausente ($interactScript)" +# HITL: via sky.ps1 interact (auditoria human.interaction.*) +$skyCli = Join-Path $PSScriptRoot 'sky.ps1' +if (-not (Test-Path $skyCli)) { + throw "HITL obrigatorio apos assess: sky.ps1 ausente ($skyCli)" } $topGap = if ($gaps.Count -gt 0) { $gaps[0] } else { $null } $promptOverride = if ($topGap) { @@ -205,4 +205,4 @@ $promptOverride = if ($topGap) { } Write-Host "" Write-Host "=== Proximo passo (interacao HITL) ===" -ForegroundColor Cyan -& $interactScript -Slug $Slug -PointId 'assess.next_action' -Prompt $promptOverride +& $skyCli interact -Slug $Slug -PointId 'assess.next_action' -Prompt $promptOverride diff --git a/scripts/sky/attach-plugin.ps1 b/scripts/sky/attach-plugin.ps1 index 94d9b9d..1f930bf 100644 --- a/scripts/sky/attach-plugin.ps1 +++ b/scripts/sky/attach-plugin.ps1 @@ -163,26 +163,29 @@ if (Test-Path $linkFile) { } } -$runAssess = $Assess -or -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. -. (Join-Path $PSScriptRoot 'interaction-catalog.ps1') -if (Test-Path $journeyPath) { - Clear-SkyJourneyPendingInteraction -JourneyPath $journeyPath +# Default: HITL brownfield.after_attach. Assess só com -Assess. +# Documentado em SKY_INTERACT.md: attach → after_attach; assess → assess.next_action. +if ($NoAssess) { + $runAssess = $false +} elseif ($Assess) { + $runAssess = $true +} else { + $runAssess = $false } +# Nao limpar pending antecipadamente — Set-SkyJourneyPendingInteraction / assess +# sobrescrevem arrival.intent so apos sucesso. Falha no meio preserva a pergunta anterior. +$skyCli = Join-Path $PSScriptRoot 'sky.ps1' if ($runAssess) { Write-Host "" & (Join-Path $PSScriptRoot 'assess-platform.ps1') -Slug $Slug -WorkspacePath $workspace } else { - $interactScript = Join-Path $PSScriptRoot 'prompt-interaction.ps1' - if (-not (Test-Path $interactScript)) { - throw "HITL obrigatorio apos attach: script ausente ($interactScript)" + if (-not (Test-Path $skyCli)) { + throw "HITL obrigatorio apos attach: sky.ps1 ausente ($skyCli)" } Write-Host "" Write-Host "=== Proximo passo (interacao HITL) ===" -ForegroundColor Cyan - & $interactScript -Slug $Slug -PointId 'brownfield.after_attach' -WorkspacePath $workspace + & $skyCli interact -Slug $Slug -PointId 'brownfield.after_attach' -WorkspacePath $workspace } Write-Host "" diff --git a/scripts/sky/check-autonomy.ps1 b/scripts/sky/check-autonomy.ps1 index 52ffb64..aecf476 100644 --- a/scripts/sky/check-autonomy.ps1 +++ b/scripts/sky/check-autonomy.ps1 @@ -29,10 +29,12 @@ $allowed = $resolution.autonomy_check.allowed $gates = @($resolution.gates_required) $actionGates = @{ - 'export.package' = @('package') + 'export.package' = @('package', 'architecture') 'publish.preview' = @() 'publish.public' = @('public_showcase', 'package') + # brief global; architecture vem de requires_gate da coreografia (architecture-spec) 'skill.invoke' = @('brief') + 'side_effect' = @('architecture', 'package') } $allGates = [System.Collections.Generic.List[string]]::new() foreach ($g in $gates) { if ($allGates -notcontains $g) { $allGates.Add($g) } } @@ -43,27 +45,32 @@ if ($actionGates.ContainsKey($Action)) { } $gates = @($allGates) -# Verificar approvals.yaml (stages:) +# Verificar approvals.yaml (stages:) — timestamp de approve-stage conta como aprovado $approvalsPath = Join-Path $RepoRoot ".sky\sessions\$Slug\approvals.yaml" $gateStatus = @{} 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 } + 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 + } else { + $gateStatus[$g] = $false + } } } $needsGate = $false $blockingGates = @() -if ($actionGates.ContainsKey($Action)) { - foreach ($g in $actionGates[$Action]) { - if (-not $gateStatus.ContainsKey($g) -or -not $gateStatus[$g]) { - $needsGate = $true - if ($blockingGates -notcontains $g) { $blockingGates += $g } - } +# Bloquear por TODOS os gates (actionGates + requires_gate da coreografia, ex.: architecture) +foreach ($g in $gates) { + if ([string]::IsNullOrWhiteSpace("$g")) { continue } + $approved = $gateStatus.ContainsKey($g) -and $gateStatus[$g] + if (-not $approved) { + $needsGate = $true + if ($blockingGates -notcontains $g) { $blockingGates += $g } } } diff --git a/scripts/sky/interaction-catalog.ps1 b/scripts/sky/interaction-catalog.ps1 index 97d1b27..212445f 100644 --- a/scripts/sky/interaction-catalog.ps1 +++ b/scripts/sky/interaction-catalog.ps1 @@ -8,21 +8,52 @@ function Escape-SkyYamlDoubleQuoted { param([AllowNull()][string]$Text) if ($null -eq $Text) { return '' } - $t = $Text -replace '\\', '\\' - $t = $t -replace '"', '\"' - $t = $t -replace "`r`n", '\n' - $t = $t -replace "`n", '\n' - $t = $t -replace "`r", '\n' + # String.Replace — um '\' vira '\\' no scalar YAML entre aspas duplas + $t = $Text.Replace('\', '\\') + $t = $t.Replace('"', '\"') + $t = $t.Replace("`r`n", '\n').Replace("`n", '\n').Replace("`r", '\n') return $t } function Unescape-SkyYamlDoubleQuoted { param([AllowNull()][string]$Text) if ($null -eq $Text) { return '' } - $t = $Text -replace '\\n', "`n" - $t = $t -replace '\\"', '"' - $t = $t -replace '\\\\', '\' - return $t + # Passo unico L→R: \\n nao vira newline apos expandir \\ + $sb = New-Object System.Text.StringBuilder + $chars = $Text.ToCharArray() + $i = 0 + while ($i -lt $chars.Length) { + if ($chars[$i] -eq '\' -and ($i + 1) -lt $chars.Length) { + $next = $chars[$i + 1] + if ($next -eq 'n') { [void]$sb.Append("`n"); $i += 2; continue } + if ($next -eq '"') { [void]$sb.Append('"'); $i += 2; continue } + if ($next -eq '\') { [void]$sb.Append('\'); $i += 2; continue } + } + [void]$sb.Append($chars[$i]) + $i++ + } + return $sb.ToString() +} + +function Skip-SkyYamlTopLevelKeyBlock { + <# + .SYNOPSIS + Avanca o indice apos um bloco YAML top-level (chave + linhas indentadas). + #> + param( + [Parameter(Mandatory = $true)][string[]]$Lines, + [Parameter(Mandatory = $true)][int]$Index + ) + $i = $Index + 1 + while ($i -lt $Lines.Count -and ($Lines[$i] -match '^\s' -or $Lines[$i] -match '^\s*$')) { + if ($Lines[$i] -match '^\s*$') { + $j = $i + 1 + while ($j -lt $Lines.Count -and $Lines[$j] -match '^\s*$') { $j++ } + if ($j -lt $Lines.Count -and $Lines[$j] -match '^[a-zA-Z_]') { break } + } + $i++ + } + return $i } function ConvertFrom-SkyInteractionPointsYaml { @@ -273,22 +304,42 @@ function Resolve-SkyInteractStage { ) if ($Explicit) { return $Explicit } $approvals = Join-Path $SessionDir 'approvals.yaml' + $order = @('brief', 'elevation', 'architecture', 'package', 'public_showcase') if (-not (Test-Path $approvals)) { return 'brief' } $raw = Get-Content $approvals -Raw - foreach ($s in @('brief', 'elevation', 'architecture', 'package', 'public_showcase')) { - if ($raw -notmatch "(?m)^\s*${s}\s*:") { - return $s + # Isolar bloco stages: — nao confundir com outras chaves homonimas + $stagesBlock = $raw + if ($raw -match '(?ms)^stages:\s*\r?\n(.*?)(?=^[a-z_][a-z0-9_]*:\s*$|\z)') { + $stagesBlock = $Matches[1] + } + foreach ($s in $order) { + $approved = $false + if ($stagesBlock -match "(?m)^\s+$([regex]::Escape($s)):\s*(.+)$") { + $val = $Matches[1].Trim().Trim('"').Trim("'") + # approve-stage.ps1 grava timestamp ISO; legado pode usar "approved …" + if ($val -match 'approved' -or $val -match '\d{4}-\d{2}-\d{2}T') { + $approved = $true + } } + if (-not $approved) { return $s } } - return 'package' + # Todos aprovados — ultimo stage da fila (nao default "package") + return 'public_showcase' } function Get-SkyMaturityTopGapOptions { param([Parameter(Mandatory = $true)][string]$MaturityPath) if (-not (Test-Path $MaturityPath)) { return @() } $raw = Get-Content $MaturityPath -Raw + # Isolar bloco dimensions: para nao ancorar em weights. + $dimsSection = $null + if ($raw -match '(?ms)^dimensions:\s*\r?\n(.*?)(?=^[a-z_][a-z0-9_]*:\s*$|\z)') { + $dimsSection = $Matches[1] + } + if (-not $dimsSection) { return @() } + $dimOrder = @('business', 'product', 'ux_design', 'technical', 'sustainability', 'elevation') $labels = @{ business = 'Negócio' @@ -300,12 +351,19 @@ function Get-SkyMaturityTopGapOptions { } $scored = @() foreach ($dim in $dimOrder) { + $block = $null + # Indent do nome da dimensao; propriedades devem ser mais indentadas (nao o proximo dim) + $dimPat = "(?ms)^([ \t]+)$([regex]::Escape($dim)):\s*\r?\n((?:^\1[ \t]+.+\r?\n)*)" + if ($dimsSection -match $dimPat) { + $block = $Matches[2] + } + if (-not $block) { continue } $score = 1.0 - if ($raw -match "(?ms)^\s+$dim`:\s*\r?\n(?:.*?\r?\n)*?\s+score:\s*([0-9.]+)") { + if ($block -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 ($block -match '(?m)^\s+gaps:\s*\[([^\]]*)\]') { $inner = $Matches[1] if ($inner -match '"([^"]+)"') { $gap = $Matches[1] } elseif ($inner -match "'([^']+)'") { $gap = $Matches[1] } @@ -421,6 +479,9 @@ function Format-SkyPendingInteractionYaml { $sets = Escape-SkyYamlDoubleQuoted $opt.sets [void]$sb.AppendLine(" sets: `"$sets`"") } + if ($opt.requires_gate) { + [void]$sb.AppendLine(" requires_gate: $($opt.requires_gate)") + } } return $sb.ToString().TrimEnd() } @@ -442,27 +503,11 @@ function Set-SkyJourneyPendingInteraction { while ($i -lt $lines.Count) { $line = $lines[$i] if ($line -match '^pending_interaction\s*:') { - $i++ - while ($i -lt $lines.Count -and ($lines[$i] -match '^\s' -or $lines[$i] -match '^\s*$')) { - if ($lines[$i] -match '^\s*$') { - $j = $i + 1 - while ($j -lt $lines.Count -and $lines[$j] -match '^\s*$') { $j++ } - if ($j -lt $lines.Count -and $lines[$j] -match '^[a-zA-Z_]') { break } - } - $i++ - } + $i = Skip-SkyYamlTopLevelKeyBlock -Lines $lines -Index $i continue } if ($line -match '^next_suggested_actions\s*:' -and $ReplaceNextActions -and $NextActionsYamlLines.Count -gt 0) { - $i++ - while ($i -lt $lines.Count -and ($lines[$i] -match '^\s' -or $lines[$i] -match '^\s*$')) { - if ($lines[$i] -match '^\s*$') { - $j = $i + 1 - while ($j -lt $lines.Count -and $lines[$j] -match '^\s*$') { $j++ } - if ($j -lt $lines.Count -and $lines[$j] -match '^[a-zA-Z_]') { break } - } - $i++ - } + $i = Skip-SkyYamlTopLevelKeyBlock -Lines $lines -Index $i $out.Add('next_suggested_actions:') foreach ($l in $NextActionsYamlLines) { $out.Add($l) } $out.Add('') @@ -514,15 +559,7 @@ function Clear-SkyJourneyPendingInteraction { while ($i -lt $lines.Count) { $line = $lines[$i] if ($line -match '^pending_interaction\s*:') { - $i++ - while ($i -lt $lines.Count -and ($lines[$i] -match '^\s' -or $lines[$i] -match '^\s*$')) { - if ($lines[$i] -match '^\s*$') { - $j = $i + 1 - while ($j -lt $lines.Count -and $lines[$j] -match '^\s*$') { $j++ } - if ($j -lt $lines.Count -and $lines[$j] -match '^[a-zA-Z_]') { break } - } - $i++ - } + $i = Skip-SkyYamlTopLevelKeyBlock -Lines $lines -Index $i continue } if ($line -match '^updated_at:') { diff --git a/scripts/sky/new-session.ps1 b/scripts/sky/new-session.ps1 index 0f539d2..f4672eb 100644 --- a/scripts/sky/new-session.ps1 +++ b/scripts/sky/new-session.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 5.1 +#Requires -Version 5.1 param( [Parameter(Mandatory = $true)] [string]$Slug @@ -146,11 +146,6 @@ pending_suggestions: [] "@ } if ($f -eq 'journey.yaml') { - . (Join-Path $PSScriptRoot 'interaction-catalog.ps1') - $arrival = (Get-SkyInteractionCatalog)['arrival.intent'] - if (-not $arrival) { throw 'Catalogo sem arrival.intent — verifique .agents/interaction-points.yaml' } - $pendingBlock = Format-SkyPendingInteractionYaml -PointId 'arrival.intent' -Prompt $arrival.prompt ` - -Options $arrival.options -Status 'pending' -Channel 'ask_question' -CreatedAt $now $content = @" version: "1.0" slug: $Slug @@ -180,8 +175,6 @@ next_suggested_actions: notes: | Criado pelo sky-intake. sky-host conduz via sky-interact (AskQuestion ou fallback numerado). - -$pendingBlock "@ } Set-Content -Path $dest -Value $content -Encoding UTF8 -NoNewline @@ -195,4 +188,11 @@ if (Test-Path $maturityPath) { Set-Content -Path $maturityPath -Value $m -Encoding UTF8 -NoNewline } +# HITL arrival.intent via CLI auditado (apos base da sessao no disco) +$skyCli = Join-Path $PSScriptRoot 'sky.ps1' +if (-not (Test-Path $skyCli)) { + throw "sky.ps1 ausente para gravar arrival.intent: $skyCli" +} +& $skyCli interact -Slug $Slug -PointId 'arrival.intent' + Write-Host "OK: sessao $Slug em $SessionDir" diff --git a/scripts/sky/prompt-interaction.ps1 b/scripts/sky/prompt-interaction.ps1 index 4459e72..44fc125 100644 --- a/scripts/sky/prompt-interaction.ps1 +++ b/scripts/sky/prompt-interaction.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 5.1 +#Requires -Version 5.1 <# .SYNOPSIS Grava / resolve / limpa pending_interaction em journey.yaml e imprime payload para o agente. @@ -134,9 +134,7 @@ if ($Resolve) { $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 ` diff --git a/scripts/sky/sky.ps1 b/scripts/sky/sky.ps1 index 3ced38b..65beef6 100644 --- a/scripts/sky/sky.ps1 +++ b/scripts/sky/sky.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 5.1 +#Requires -Version 5.1 <# .SYNOPSIS CLI principal do Sky-Forge. @@ -360,6 +360,9 @@ switch ($Command) { } } 'attach' { + if ($Assess -and $NoAssess) { + throw 'attach: nao combine -Assess e -NoAssess' + } $attachArgs = @{} if ($Slug) { $attachArgs.Slug = $Slug } if ($WorkspacePath) { $attachArgs.WorkspacePath = $WorkspacePath } @@ -389,18 +392,32 @@ switch ($Command) { } 'interact' { if (-not $Slug) { throw 'interact requires -Slug' } + if ($Clear -and $Resolve) { + throw 'interact: nao combine -Clear e -Resolve' + } + if ($Clear) { + if ($ChoiceId) { throw 'interact -Clear nao aceita -ChoiceId' } + if ($PointId) { throw 'interact -Clear nao aceita -PointId' } + if ($Prompt) { throw 'interact -Clear nao aceita -Prompt' } + } + elseif ($Resolve) { + if ($Prompt) { throw 'interact -Resolve nao aceita -Prompt' } + if (-not $ChoiceId) { throw 'interact -Resolve requires -ChoiceId' } + } + else { + if (-not $PointId) { throw 'interact requires -PointId (ou -Clear / -Resolve)' } + if ($ChoiceId) { throw 'interact com -PointId nao aceita -ChoiceId (use -Resolve)' } + } $iArgs = @{ Slug = $Slug } if ($WorkspacePath) { $iArgs.WorkspacePath = $WorkspacePath } if ($Stage) { $iArgs.Stage = $Stage } if ($Clear) { $iArgs.Clear = $true } elseif ($Resolve) { $iArgs.Resolve = $true - if (-not $ChoiceId) { throw 'interact -Resolve requires -ChoiceId' } $iArgs.ChoiceId = $ChoiceId if ($PointId) { $iArgs.PointId = $PointId } } else { - if (-not $PointId) { throw 'interact requires -PointId (ou -Clear / -Resolve)' } $iArgs.PointId = $PointId if ($Prompt) { $iArgs.Prompt = $Prompt } } diff --git a/templates/sessions/example-horta/journey.yaml b/templates/sessions/example-horta/journey.yaml index 22066e7..26eb31d 100644 --- a/templates/sessions/example-horta/journey.yaml +++ b/templates/sessions/example-horta/journey.yaml @@ -38,7 +38,7 @@ pending_interaction: 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)" From b94b50132e35a4080c84efd58894c896d0249222 Mon Sep 17 00:00:00 2001 From: Raphael Date: Wed, 29 Jul 2026 01:03:29 -0300 Subject: [PATCH 6/6] fix(interact): restore UTF-8 BOM on PowerShell HITL scripts PS 5.1 and PSScriptAnalyzer need BOM for non-ASCII Portuguese strings in attach-plugin and interaction-catalog. Co-authored-by: Cursor --- scripts/sky/attach-plugin.ps1 | 2 +- scripts/sky/interaction-catalog.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/sky/attach-plugin.ps1 b/scripts/sky/attach-plugin.ps1 index 1f930bf..98053f5 100644 --- a/scripts/sky/attach-plugin.ps1 +++ b/scripts/sky/attach-plugin.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 5.1 +#Requires -Version 5.1 <# .SYNOPSIS Instala Sky-Forge Host Plugin num repo brownfield existente. diff --git a/scripts/sky/interaction-catalog.ps1 b/scripts/sky/interaction-catalog.ps1 index 212445f..e26a207 100644 --- a/scripts/sky/interaction-catalog.ps1 +++ b/scripts/sky/interaction-catalog.ps1 @@ -1,4 +1,4 @@ -#Requires -Version 5.1 +#Requires -Version 5.1 <# .SYNOPSIS Catálogo de pontos de interação — fonte: .agents/interaction-points.yaml