From e99e360e5e903e1c78d23591b430b2c99271b168 Mon Sep 17 00:00:00 2001 From: Adam Scerra Date: Thu, 18 Jun 2026 12:54:21 -0400 Subject: [PATCH 1/8] feat: add explore, refine, critique refinement pipeline agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three new first-class agents forming a chained refinement pipeline: explore → refine → critique, with artifact-based data passing and revision loops (max 3 rounds). Includes: Go role registration, GitHub App configs, scaffold files (agents, harness, policies, schemas, scripts, skills, env, workflows), reusable workflows, Jira integration with credential isolation, JIRA_PROJECT_VISIBILITY safety check, shared pipeline-helpers.sh, and two ADRs (0049 pipeline architecture, 0050 Jira integration). Co-authored-by: Cursor Signed-off-by: Adam Scerra --- .../workflows/reusable-create-children.yml | 173 ++++++++ .github/workflows/reusable-critique.yml | 191 +++++++++ .github/workflows/reusable-explore.yml | 172 ++++++++ .github/workflows/reusable-refine.yml | 202 +++++++++ Makefile | 5 + .../0049-refinement-pipeline-architecture.md | 86 ++++ docs/ADRs/0050-jira-integration-model.md | 97 +++++ docs/agents/README.md | 3 + docs/agents/critique.md | 69 +++ docs/agents/explore.md | 58 +++ docs/agents/refine.md | 60 +++ internal/config/config.go | 6 +- internal/config/config_test.go | 10 +- internal/forge/github/types.go | 29 +- internal/forge/github/types_test.go | 40 +- internal/harness/scaffold_integration_test.go | 7 +- internal/scaffold/baseurl_test.go | 2 +- .../.github/workflows/create-children.yml | 60 +++ .../.github/workflows/critique.yml | 64 +++ .../.github/workflows/explore.yml | 65 +++ .../.github/workflows/jira-comment-poller.yml | 149 +++++++ .../.github/workflows/jira-dispatch.yml | 185 ++++++++ .../.github/workflows/refine-dispatch.yml | 200 +++++++++ .../.github/workflows/refine.yml | 91 ++++ .../scaffold/fullsend-repo/agents/critique.md | 404 ++++++++++++++++++ .../scaffold/fullsend-repo/agents/explore.md | 249 +++++++++++ .../scaffold/fullsend-repo/agents/refine.md | 385 +++++++++++++++++ .../fullsend-repo/harness/critique.yaml | 65 +++ .../fullsend-repo/harness/explore.yaml | 55 +++ .../fullsend-repo/harness/refine.yaml | 64 +++ .../fullsend-repo/policies/critique.yaml | 54 +++ .../fullsend-repo/policies/explore.yaml | 71 +++ .../fullsend-repo/policies/refine.yaml | 54 +++ .../schemas/critique-result.schema.json | 135 ++++++ .../schemas/explore-result.schema.json | 97 +++++ .../schemas/refine-result.schema.json | 94 ++++ .../scripts/create-children-test.sh | 207 +++++++++ .../fullsend-repo/scripts/create-children.sh | 378 ++++++++++++++++ .../fullsend-repo/scripts/markdown-to-adf.py | 242 +++++++++++ .../fullsend-repo/scripts/pipeline-events.sh | 129 ++++++ .../fullsend-repo/scripts/pipeline-helpers.sh | 97 +++++ .../scripts/post-critique-test.sh | 216 ++++++++++ .../fullsend-repo/scripts/post-critique.sh | 269 ++++++++++++ .../scripts/post-explore-test.sh | 170 ++++++++ .../fullsend-repo/scripts/post-explore.sh | 134 ++++++ .../fullsend-repo/scripts/post-refine-test.sh | 172 ++++++++ .../fullsend-repo/scripts/post-refine.sh | 196 +++++++++ .../fullsend-repo/scripts/pre-critique.sh | 143 +++++++ .../fullsend-repo/scripts/pre-explore.sh | 313 ++++++++++++++ .../fullsend-repo/scripts/pre-refine.sh | 188 ++++++++ .../scripts/sanitize-artifacts-test.sh | 119 ++++++ .../scripts/sanitize-artifacts.sh | 236 ++++++++++ .../fullsend-repo/skills/jira-read/SKILL.md | 106 +++++ .../skills/public-research/SKILL.md | 97 +++++ internal/scaffold/render.go | 4 + internal/scaffold/scaffold.go | 16 + internal/scaffold/scaffold_test.go | 363 ++++++++++++++++ internal/scaffold/vendormanifest.go | 4 + .../scaffold/workflow_call_alignment_test.go | 8 +- 59 files changed, 7545 insertions(+), 13 deletions(-) create mode 100644 .github/workflows/reusable-create-children.yml create mode 100644 .github/workflows/reusable-critique.yml create mode 100644 .github/workflows/reusable-explore.yml create mode 100644 .github/workflows/reusable-refine.yml create mode 100644 docs/ADRs/0049-refinement-pipeline-architecture.md create mode 100644 docs/ADRs/0050-jira-integration-model.md create mode 100644 docs/agents/critique.md create mode 100644 docs/agents/explore.md create mode 100644 docs/agents/refine.md create mode 100644 internal/scaffold/fullsend-repo/.github/workflows/create-children.yml create mode 100644 internal/scaffold/fullsend-repo/.github/workflows/critique.yml create mode 100644 internal/scaffold/fullsend-repo/.github/workflows/explore.yml create mode 100644 internal/scaffold/fullsend-repo/.github/workflows/jira-comment-poller.yml create mode 100644 internal/scaffold/fullsend-repo/.github/workflows/jira-dispatch.yml create mode 100644 internal/scaffold/fullsend-repo/.github/workflows/refine-dispatch.yml create mode 100644 internal/scaffold/fullsend-repo/.github/workflows/refine.yml create mode 100644 internal/scaffold/fullsend-repo/agents/critique.md create mode 100644 internal/scaffold/fullsend-repo/agents/explore.md create mode 100644 internal/scaffold/fullsend-repo/agents/refine.md create mode 100644 internal/scaffold/fullsend-repo/harness/critique.yaml create mode 100644 internal/scaffold/fullsend-repo/harness/explore.yaml create mode 100644 internal/scaffold/fullsend-repo/harness/refine.yaml create mode 100644 internal/scaffold/fullsend-repo/policies/critique.yaml create mode 100644 internal/scaffold/fullsend-repo/policies/explore.yaml create mode 100644 internal/scaffold/fullsend-repo/policies/refine.yaml create mode 100644 internal/scaffold/fullsend-repo/schemas/critique-result.schema.json create mode 100644 internal/scaffold/fullsend-repo/schemas/explore-result.schema.json create mode 100644 internal/scaffold/fullsend-repo/schemas/refine-result.schema.json create mode 100755 internal/scaffold/fullsend-repo/scripts/create-children-test.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/create-children.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/markdown-to-adf.py create mode 100755 internal/scaffold/fullsend-repo/scripts/pipeline-events.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/pipeline-helpers.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/post-critique-test.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/post-critique.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/post-explore-test.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/post-explore.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/post-refine-test.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/post-refine.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/pre-critique.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/pre-explore.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/pre-refine.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/sanitize-artifacts-test.sh create mode 100755 internal/scaffold/fullsend-repo/scripts/sanitize-artifacts.sh create mode 100644 internal/scaffold/fullsend-repo/skills/jira-read/SKILL.md create mode 100644 internal/scaffold/fullsend-repo/skills/public-research/SKILL.md diff --git a/.github/workflows/reusable-create-children.yml b/.github/workflows/reusable-create-children.yml new file mode 100644 index 000000000..224bf1c68 --- /dev/null +++ b/.github/workflows/reusable-create-children.yml @@ -0,0 +1,173 @@ +name: Create Children + +on: + workflow_call: + inputs: + event_type: + required: true + type: string + source_repo: + required: true + type: string + event_payload: + required: true + type: string + mint_url: + required: true + type: string + gcp_region: + required: true + type: string + fullsend_version: + required: false + type: string + default: "latest" + install_mode: + required: false + type: string + default: "per-org" + fullsend_ai_ref: + description: Ref of fullsend-ai/fullsend to load actions from. Must match the ref used in the `uses:` line that calls this workflow. + type: string + required: false + default: v0 + issue_key: + required: true + type: string + issue_source: + required: false + type: string + default: "github" + refine_run_id: + required: true + type: string + secrets: + FULLSEND_GCP_WIF_PROVIDER: + required: true + FULLSEND_GCP_PROJECT_ID: + required: true + JIRA_HOST: + required: false + JIRA_EMAIL: + required: false + JIRA_API_TOKEN: + required: false + +jobs: + create-children: + name: Create Children + runs-on: ubuntu-latest + permissions: + actions: write + contents: read + id-token: write + issues: write + + steps: + - name: Checkout config repository + uses: actions/checkout@v6 + + - name: Checkout upstream defaults + if: hashFiles('.defaults/action.yml', '.fullsend/.defaults/action.yml') == '' + uses: actions/checkout@v6 + with: + repository: fullsend-ai/fullsend + ref: ${{ inputs.fullsend_ai_ref }} + path: .defaults + fetch-depth: 1 + sparse-checkout: | + .github/actions/ + .github/scripts/ + internal/scaffold/fullsend-repo/ + action.yml + + - name: Prepare workspace (upstream defaults + org/repo overrides) + env: + INSTALL_MODE: ${{ inputs.install_mode }} + run: | + set -euo pipefail + if [[ "${INSTALL_MODE}" != "per-org" && "${INSTALL_MODE}" != "per-repo" ]]; then + echo "::error::Invalid install_mode '${INSTALL_MODE}': must be 'per-org' or 'per-repo'" + exit 1 + fi + SRC=".defaults/internal/scaffold/fullsend-repo" + LAYERED_DIRS="agents skills schemas harness plugins policies scripts env" + for dir in ${LAYERED_DIRS}; do + if [[ -d "${SRC}/${dir}" ]]; then + mkdir -p "${dir}" + cp -r "${SRC}/${dir}/." "${dir}/" + fi + done + CUSTOM_BASE="customized" + if [[ "${INSTALL_MODE}" == "per-repo" ]]; then + CUSTOM_BASE=".fullsend/customized" + fi + for dir in ${LAYERED_DIRS}; do + if [[ -d "${CUSTOM_BASE}/${dir}" ]]; then + find "${CUSTOM_BASE}/${dir}" -type f ! -name '.gitkeep' -print0 \ + | while IFS= read -r -d '' f; do + rel="${f#"${CUSTOM_BASE}"/}" + mkdir -p "$(dirname "${rel}")" + cp "${f}" "${rel}" + done + fi + done + mkdir -p .github/scripts + cp "${SRC}/.github/scripts/setup-agent-env.sh" .github/scripts/setup-agent-env.sh + + + - name: Validate enrollment and extract repo metadata + id: repo-parts + uses: ./.defaults/.github/actions/validate-enrollment + with: + source_repo: ${{ inputs.source_repo }} + install_mode: ${{ inputs.install_mode }} + + - name: Mint create-children token + id: app-token + uses: ./.defaults/.github/actions/mint-token + with: + role: refine + repos: ${{ steps.repo-parts.outputs.name }} + mint_url: ${{ inputs.mint_url }} + + - name: Checkout target repository + uses: actions/checkout@v6 + with: + repository: ${{ inputs.source_repo }} + token: ${{ steps.app-token.outputs.token }} + path: target-repo + fetch-depth: 1 + persist-credentials: false + + - name: Setup GCP and prepare credentials + uses: ./.defaults/.github/actions/setup-gcp + with: + gcp_wif_provider: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }} + gcp_project_id: ${{ secrets.FULLSEND_GCP_PROJECT_ID }} + + - name: Setup agent environment + env: + AGENT_PREFIX: CREATE_CHILDREN_ + CREATE_CHILDREN_GH_TOKEN: ${{ steps.app-token.outputs.token }} + CREATE_CHILDREN_TARGET_REPO_DIR: target-repo + CREATE_CHILDREN_ISSUE_KEY: ${{ inputs.issue_key }} + CREATE_CHILDREN_ISSUE_SOURCE: ${{ inputs.issue_source }} + CREATE_CHILDREN_REFINE_RUN_ID: ${{ inputs.refine_run_id }} + CREATE_CHILDREN_ANTHROPIC_VERTEX_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }} + CREATE_CHILDREN_CLOUD_ML_REGION: ${{ inputs.gcp_region }} + JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + run: bash .github/scripts/setup-agent-env.sh + + - name: Run create-children script + env: + ISSUE_KEY: ${{ inputs.issue_key }} + ISSUE_SOURCE: ${{ inputs.issue_source }} + REFINE_RUN_ID: ${{ inputs.refine_run_id }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + run: bash scripts/create-children.sh diff --git a/.github/workflows/reusable-critique.yml b/.github/workflows/reusable-critique.yml new file mode 100644 index 000000000..76055c932 --- /dev/null +++ b/.github/workflows/reusable-critique.yml @@ -0,0 +1,191 @@ +name: Critique Agent + +on: + workflow_call: + inputs: + event_type: + required: true + type: string + source_repo: + required: true + type: string + event_payload: + required: true + type: string + mint_url: + required: true + type: string + gcp_region: + required: true + type: string + fullsend_version: + required: false + type: string + default: "latest" + install_mode: + required: false + type: string + default: "per-org" + fullsend_ai_ref: + description: Ref of fullsend-ai/fullsend to load actions from. Must match the ref used in the `uses:` line that calls this workflow. + type: string + required: false + default: v0 + issue_key: + required: true + type: string + issue_source: + required: false + type: string + default: "github" + refine_run_id: + required: true + type: string + review_round: + required: false + type: string + default: "1" + max_review_rounds: + required: false + type: string + default: "3" + auto_create: + required: false + type: string + default: "false" + jira_project_visibility: + required: false + type: string + default: "private" + secrets: + FULLSEND_GCP_WIF_PROVIDER: + required: true + FULLSEND_GCP_PROJECT_ID: + required: true + JIRA_HOST: + required: false + JIRA_EMAIL: + required: false + JIRA_API_TOKEN: + required: false + +jobs: + critique: + name: Critique + runs-on: ubuntu-latest + permissions: + actions: write + contents: read + id-token: write + issues: write + + steps: + - name: Checkout config repository + uses: actions/checkout@v6 + + - name: Checkout upstream defaults + if: hashFiles('.defaults/action.yml', '.fullsend/.defaults/action.yml') == '' + uses: actions/checkout@v6 + with: + repository: fullsend-ai/fullsend + ref: ${{ inputs.fullsend_ai_ref }} + path: .defaults + fetch-depth: 1 + sparse-checkout: | + .github/actions/ + .github/scripts/ + internal/scaffold/fullsend-repo/ + action.yml + + - name: Prepare workspace (upstream defaults + org/repo overrides) + env: + INSTALL_MODE: ${{ inputs.install_mode }} + run: | + set -euo pipefail + if [[ "${INSTALL_MODE}" != "per-org" && "${INSTALL_MODE}" != "per-repo" ]]; then + echo "::error::Invalid install_mode '${INSTALL_MODE}': must be 'per-org' or 'per-repo'" + exit 1 + fi + SRC=".defaults/internal/scaffold/fullsend-repo" + LAYERED_DIRS="agents skills schemas harness plugins policies scripts env" + for dir in ${LAYERED_DIRS}; do + if [[ -d "${SRC}/${dir}" ]]; then + mkdir -p "${dir}" + cp -r "${SRC}/${dir}/." "${dir}/" + fi + done + CUSTOM_BASE="customized" + if [[ "${INSTALL_MODE}" == "per-repo" ]]; then + CUSTOM_BASE=".fullsend/customized" + fi + for dir in ${LAYERED_DIRS}; do + if [[ -d "${CUSTOM_BASE}/${dir}" ]]; then + find "${CUSTOM_BASE}/${dir}" -type f ! -name '.gitkeep' -print0 \ + | while IFS= read -r -d '' f; do + rel="${f#"${CUSTOM_BASE}"/}" + mkdir -p "$(dirname "${rel}")" + cp "${f}" "${rel}" + done + fi + done + mkdir -p .github/scripts + cp "${SRC}/.github/scripts/setup-agent-env.sh" .github/scripts/setup-agent-env.sh + + + - name: Validate enrollment and extract repo metadata + id: repo-parts + uses: ./.defaults/.github/actions/validate-enrollment + with: + source_repo: ${{ inputs.source_repo }} + install_mode: ${{ inputs.install_mode }} + + - name: Mint critique token + id: app-token + uses: ./.defaults/.github/actions/mint-token + with: + role: critique + repos: ${{ steps.repo-parts.outputs.name }} + mint_url: ${{ inputs.mint_url }} + + - name: Checkout target repository + uses: actions/checkout@v6 + with: + repository: ${{ inputs.source_repo }} + token: ${{ steps.app-token.outputs.token }} + path: target-repo + fetch-depth: 1 + persist-credentials: false + + - name: Setup GCP and prepare credentials + uses: ./.defaults/.github/actions/setup-gcp + with: + gcp_wif_provider: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }} + gcp_project_id: ${{ secrets.FULLSEND_GCP_PROJECT_ID }} + + - name: Setup agent environment + env: + AGENT_PREFIX: CRITIQUE_ + CRITIQUE_GH_TOKEN: ${{ steps.app-token.outputs.token }} + CRITIQUE_TARGET_REPO_DIR: target-repo + CRITIQUE_ISSUE_KEY: ${{ inputs.issue_key }} + CRITIQUE_ISSUE_SOURCE: ${{ inputs.issue_source }} + CRITIQUE_REFINE_RUN_ID: ${{ inputs.refine_run_id }} + CRITIQUE_REVIEW_ROUND: ${{ inputs.review_round }} + CRITIQUE_MAX_REVIEW_ROUNDS: ${{ inputs.max_review_rounds }} + CRITIQUE_AUTO_CREATE: ${{ inputs.auto_create }} + CRITIQUE_ANTHROPIC_VERTEX_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }} + CRITIQUE_CLOUD_ML_REGION: ${{ inputs.gcp_region }} + JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_PROJECT_VISIBILITY: ${{ inputs.jira_project_visibility }} + run: bash .github/scripts/setup-agent-env.sh + + - name: Run critique agent + uses: ./.defaults/ + with: + agent: critique + version: ${{ inputs.fullsend_version }} + run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + status-repo: ${{ inputs.source_repo }} + mint-url: ${{ inputs.mint_url }} diff --git a/.github/workflows/reusable-explore.yml b/.github/workflows/reusable-explore.yml new file mode 100644 index 000000000..633c37b5f --- /dev/null +++ b/.github/workflows/reusable-explore.yml @@ -0,0 +1,172 @@ +name: Explore Agent + +on: + workflow_call: + inputs: + event_type: + required: true + type: string + source_repo: + required: true + type: string + event_payload: + required: true + type: string + mint_url: + required: true + type: string + gcp_region: + required: true + type: string + fullsend_version: + required: false + type: string + default: "latest" + install_mode: + required: false + type: string + default: "per-org" + fullsend_ai_ref: + description: Ref of fullsend-ai/fullsend to load actions from. Must match the ref used in the `uses:` line that calls this workflow. + type: string + required: false + default: v0 + issue_key: + required: true + type: string + issue_source: + required: false + type: string + default: "github" + jira_project_visibility: + required: false + type: string + default: "private" + secrets: + FULLSEND_GCP_WIF_PROVIDER: + required: true + FULLSEND_GCP_PROJECT_ID: + required: true + JIRA_HOST: + required: false + JIRA_EMAIL: + required: false + JIRA_API_TOKEN: + required: false + +jobs: + explore: + name: Explore + runs-on: ubuntu-latest + permissions: + actions: write + contents: read + id-token: write + issues: write + + steps: + - name: Checkout config repository + uses: actions/checkout@v6 + + - name: Checkout upstream defaults + if: hashFiles('.defaults/action.yml', '.fullsend/.defaults/action.yml') == '' + uses: actions/checkout@v6 + with: + repository: fullsend-ai/fullsend + ref: ${{ inputs.fullsend_ai_ref }} + path: .defaults + fetch-depth: 1 + sparse-checkout: | + .github/actions/ + .github/scripts/ + internal/scaffold/fullsend-repo/ + action.yml + + - name: Prepare workspace (upstream defaults + org/repo overrides) + env: + INSTALL_MODE: ${{ inputs.install_mode }} + run: | + set -euo pipefail + if [[ "${INSTALL_MODE}" != "per-org" && "${INSTALL_MODE}" != "per-repo" ]]; then + echo "::error::Invalid install_mode '${INSTALL_MODE}': must be 'per-org' or 'per-repo'" + exit 1 + fi + SRC=".defaults/internal/scaffold/fullsend-repo" + LAYERED_DIRS="agents skills schemas harness plugins policies scripts env" + for dir in ${LAYERED_DIRS}; do + if [[ -d "${SRC}/${dir}" ]]; then + mkdir -p "${dir}" + cp -r "${SRC}/${dir}/." "${dir}/" + fi + done + CUSTOM_BASE="customized" + if [[ "${INSTALL_MODE}" == "per-repo" ]]; then + CUSTOM_BASE=".fullsend/customized" + fi + for dir in ${LAYERED_DIRS}; do + if [[ -d "${CUSTOM_BASE}/${dir}" ]]; then + find "${CUSTOM_BASE}/${dir}" -type f ! -name '.gitkeep' -print0 \ + | while IFS= read -r -d '' f; do + rel="${f#"${CUSTOM_BASE}"/}" + mkdir -p "$(dirname "${rel}")" + cp "${f}" "${rel}" + done + fi + done + mkdir -p .github/scripts + cp "${SRC}/.github/scripts/setup-agent-env.sh" .github/scripts/setup-agent-env.sh + + + - name: Validate enrollment and extract repo metadata + id: repo-parts + uses: ./.defaults/.github/actions/validate-enrollment + with: + source_repo: ${{ inputs.source_repo }} + install_mode: ${{ inputs.install_mode }} + + - name: Mint explore token + id: app-token + uses: ./.defaults/.github/actions/mint-token + with: + role: explore + repos: ${{ steps.repo-parts.outputs.name }} + mint_url: ${{ inputs.mint_url }} + + - name: Checkout target repository + uses: actions/checkout@v6 + with: + repository: ${{ inputs.source_repo }} + token: ${{ steps.app-token.outputs.token }} + path: target-repo + fetch-depth: 1 + persist-credentials: false + + - name: Setup GCP and prepare credentials + uses: ./.defaults/.github/actions/setup-gcp + with: + gcp_wif_provider: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }} + gcp_project_id: ${{ secrets.FULLSEND_GCP_PROJECT_ID }} + + - name: Setup agent environment + env: + AGENT_PREFIX: EXPLORE_ + EXPLORE_GH_TOKEN: ${{ steps.app-token.outputs.token }} + EXPLORE_TARGET_REPO_DIR: target-repo + EXPLORE_ISSUE_KEY: ${{ inputs.issue_key }} + EXPLORE_ISSUE_SOURCE: ${{ inputs.issue_source }} + EXPLORE_ANTHROPIC_VERTEX_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }} + EXPLORE_CLOUD_ML_REGION: ${{ inputs.gcp_region }} + JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_PROJECT_VISIBILITY: ${{ inputs.jira_project_visibility }} + run: bash .github/scripts/setup-agent-env.sh + + - name: Run explore agent + uses: ./.defaults/ + with: + agent: explore + version: ${{ inputs.fullsend_version }} + run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + status-repo: ${{ inputs.source_repo }} + mint-url: ${{ inputs.mint_url }} diff --git a/.github/workflows/reusable-refine.yml b/.github/workflows/reusable-refine.yml new file mode 100644 index 000000000..f453db03b --- /dev/null +++ b/.github/workflows/reusable-refine.yml @@ -0,0 +1,202 @@ +name: Refine Agent + +on: + workflow_call: + inputs: + event_type: + required: true + type: string + source_repo: + required: true + type: string + event_payload: + required: true + type: string + mint_url: + required: true + type: string + gcp_region: + required: true + type: string + fullsend_version: + required: false + type: string + default: "latest" + install_mode: + required: false + type: string + default: "per-org" + fullsend_ai_ref: + description: Ref of fullsend-ai/fullsend to load actions from. Must match the ref used in the `uses:` line that calls this workflow. + type: string + required: false + default: v0 + issue_key: + required: true + type: string + issue_source: + required: false + type: string + default: "github" + explore_run_id: + required: false + type: string + default: "" + explore_context_ref: + required: false + type: string + default: "" + review_round: + required: false + type: string + default: "1" + max_review_rounds: + required: false + type: string + default: "3" + auto_create: + required: false + type: string + default: "false" + critique_run_id: + required: false + type: string + default: "" + jira_project_visibility: + required: false + type: string + default: "private" + secrets: + FULLSEND_GCP_WIF_PROVIDER: + required: true + FULLSEND_GCP_PROJECT_ID: + required: true + JIRA_HOST: + required: false + JIRA_EMAIL: + required: false + JIRA_API_TOKEN: + required: false + +jobs: + refine: + name: Refine + runs-on: ubuntu-latest + permissions: + actions: write + contents: read + id-token: write + issues: write + + steps: + - name: Checkout config repository + uses: actions/checkout@v6 + + - name: Checkout upstream defaults + if: hashFiles('.defaults/action.yml', '.fullsend/.defaults/action.yml') == '' + uses: actions/checkout@v6 + with: + repository: fullsend-ai/fullsend + ref: ${{ inputs.fullsend_ai_ref }} + path: .defaults + fetch-depth: 1 + sparse-checkout: | + .github/actions/ + .github/scripts/ + internal/scaffold/fullsend-repo/ + action.yml + + - name: Prepare workspace (upstream defaults + org/repo overrides) + env: + INSTALL_MODE: ${{ inputs.install_mode }} + run: | + set -euo pipefail + if [[ "${INSTALL_MODE}" != "per-org" && "${INSTALL_MODE}" != "per-repo" ]]; then + echo "::error::Invalid install_mode '${INSTALL_MODE}': must be 'per-org' or 'per-repo'" + exit 1 + fi + SRC=".defaults/internal/scaffold/fullsend-repo" + LAYERED_DIRS="agents skills schemas harness plugins policies scripts env" + for dir in ${LAYERED_DIRS}; do + if [[ -d "${SRC}/${dir}" ]]; then + mkdir -p "${dir}" + cp -r "${SRC}/${dir}/." "${dir}/" + fi + done + CUSTOM_BASE="customized" + if [[ "${INSTALL_MODE}" == "per-repo" ]]; then + CUSTOM_BASE=".fullsend/customized" + fi + for dir in ${LAYERED_DIRS}; do + if [[ -d "${CUSTOM_BASE}/${dir}" ]]; then + find "${CUSTOM_BASE}/${dir}" -type f ! -name '.gitkeep' -print0 \ + | while IFS= read -r -d '' f; do + rel="${f#"${CUSTOM_BASE}"/}" + mkdir -p "$(dirname "${rel}")" + cp "${f}" "${rel}" + done + fi + done + mkdir -p .github/scripts + cp "${SRC}/.github/scripts/setup-agent-env.sh" .github/scripts/setup-agent-env.sh + + + - name: Validate enrollment and extract repo metadata + id: repo-parts + uses: ./.defaults/.github/actions/validate-enrollment + with: + source_repo: ${{ inputs.source_repo }} + install_mode: ${{ inputs.install_mode }} + + - name: Mint refine token + id: app-token + uses: ./.defaults/.github/actions/mint-token + with: + role: refine + repos: ${{ steps.repo-parts.outputs.name }} + mint_url: ${{ inputs.mint_url }} + + - name: Checkout target repository + uses: actions/checkout@v6 + with: + repository: ${{ inputs.source_repo }} + token: ${{ steps.app-token.outputs.token }} + path: target-repo + fetch-depth: 1 + persist-credentials: false + + - name: Setup GCP and prepare credentials + uses: ./.defaults/.github/actions/setup-gcp + with: + gcp_wif_provider: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }} + gcp_project_id: ${{ secrets.FULLSEND_GCP_PROJECT_ID }} + + - name: Setup agent environment + env: + AGENT_PREFIX: REFINE_ + REFINE_GH_TOKEN: ${{ steps.app-token.outputs.token }} + REFINE_TARGET_REPO_DIR: target-repo + REFINE_ISSUE_KEY: ${{ inputs.issue_key }} + REFINE_ISSUE_SOURCE: ${{ inputs.issue_source }} + REFINE_EXPLORE_RUN_ID: ${{ inputs.explore_run_id }} + REFINE_EXPLORE_CONTEXT_REF: ${{ inputs.explore_context_ref }} + REFINE_REVIEW_ROUND: ${{ inputs.review_round }} + REFINE_MAX_REVIEW_ROUNDS: ${{ inputs.max_review_rounds }} + REFINE_AUTO_CREATE: ${{ inputs.auto_create }} + REFINE_CRITIQUE_RUN_ID: ${{ inputs.critique_run_id }} + REFINE_ANTHROPIC_VERTEX_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }} + REFINE_CLOUD_ML_REGION: ${{ inputs.gcp_region }} + JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + JIRA_PROJECT_VISIBILITY: ${{ inputs.jira_project_visibility }} + run: bash .github/scripts/setup-agent-env.sh + + - name: Run refine agent + uses: ./.defaults/ + with: + agent: refine + version: ${{ inputs.fullsend_version }} + run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + status-repo: ${{ inputs.source_repo }} + mint-url: ${{ inputs.mint_url }} diff --git a/Makefile b/Makefile index 43d4f927d..6357cbfb4 100644 --- a/Makefile +++ b/Makefile @@ -115,6 +115,11 @@ script-test: bash internal/scaffold/fullsend-repo/scripts/validate-output-schema-test.sh bash internal/scaffold/fullsend-repo/scripts/pre-code-test.sh bash internal/scaffold/fullsend-repo/scripts/pre-fetch-prior-review-test.sh + bash internal/scaffold/fullsend-repo/scripts/post-explore-test.sh + bash internal/scaffold/fullsend-repo/scripts/post-refine-test.sh + bash internal/scaffold/fullsend-repo/scripts/post-critique-test.sh + bash internal/scaffold/fullsend-repo/scripts/create-children-test.sh + bash internal/scaffold/fullsend-repo/scripts/sanitize-artifacts-test.sh python3 internal/scaffold/fullsend-repo/scripts/process-fix-result-test.py python3 skills/topissues/scripts/topissues_test.py diff --git a/docs/ADRs/0049-refinement-pipeline-architecture.md b/docs/ADRs/0049-refinement-pipeline-architecture.md new file mode 100644 index 000000000..9beb8cb18 --- /dev/null +++ b/docs/ADRs/0049-refinement-pipeline-architecture.md @@ -0,0 +1,86 @@ +# ADR 0049: Refinement Pipeline Architecture + +## Status + +Accepted + +## Context + +fullsend currently ships 6 standalone agents (triage, code, review, fix, retro, prioritize), each triggered by a single event and producing a single output. The Feature Refinement Working Group has developed and validated a multi-stage pipeline that decomposes high-level issues into implementable work items through exploration, refinement, and quality-gated critique. + +This pipeline extends fullsend's existing patterns: + +1. **Chained agents with artifact passing** (new): Existing agents are standalone — triage triggers code via a `ready-to-code` label, which fires a webhook, and the code agent reads the issue fresh from GitHub. No data passes between them. The refinement pipeline is different because each stage produces structured context that the next stage needs: explore produces `exploration_context.json` (~50-100KB of analyzed research), refine produces `refine-result.json` (the full decomposition plan with children, acceptance criteria, etc.), and critique needs both to evaluate the plan. Labels can signal "go/no-go" but can't carry a run ID for artifact correlation, so we use explicit `workflow_dispatch` chaining instead. For example, post-explore.sh chains to refine like this: + + ``` + gh workflow run refine.yml \ + --repo "$WORKFLOW_REPO" \ + -f issue_key="PROJ-123" \ + -f issue_source="jira" \ + -f explore_run_id="$GITHUB_RUN_ID" + ``` + + The refine pre-script then downloads explore's artifact using that run ID: + + ``` + gh run download "$EXPLORE_RUN_ID" \ + --repo "$REPO" \ + --name "fullsend-explore" \ + --dir "$ARTIFACT_DIR" + ``` + +2. **Revision loops** (extension of review → fix): Critique can send work back to refine for up to N rounds (default 3) before escalating to a human. This mirrors the existing review → fix iteration pattern (`FIX_ITERATION` cap, default 5) but uses `workflow_dispatch` chaining instead of PR event triggers, since the critique feedback artifact needs to be passed back to refine. + +The pipeline has been validated in fullsend-ai/features as custom agents over several weeks with consistent quality improvements in issue decomposition and reduced manual refinement time. + +## Decision + +We will add three new agent roles to the fullsend scaffold: **explore**, **refine**, and **critique**. These agents form a single refinement pipeline, triggered by the `/fs-refine` command on an issue. + +This is a **Phase 1 implementation** focused on getting the pipeline functional and available to all fullsend installations. Later phases will harden, optimize, and extend it based on production feedback. + +### Phase 1 decisions (this PR) + +**Separate GitHub Apps**: Each agent (explore, refine, critique) gets its own GitHub App, consistent with every other fullsend agent. For the GitHub workflow, this provides a clear audit trail — each agent's comments and actions appear under a distinct bot identity. It also enables granular access control and avoids coupling permissions if the agents' needs diverge. Note: for the Jira workflow, all three agents share the same `JIRA_EMAIL` service account, so Jira comments are attributed to a single bot user regardless of which agent posted them. + +**Default enabled, command-gated**: The agents ship with every fullsend installation but only activate when a user explicitly invokes `/fs-refine`. There are no automatic triggers — the pipeline never runs unless a human requests it. + +**Pipeline chaining via workflow_dispatch** (not event-driven): Existing agent chains use labels + webhooks (e.g., triage adds `ready-to-code` → code agent triggers on `issues.labeled`). This works when agents are independent — code doesn't need triage's output, it reads the issue directly. The refinement pipeline uses explicit `gh workflow run` instead because each stage needs the previous stage's artifact, identified by run ID. Labels can't carry this correlation data. + +**Revision loop with hard cap**: Critique can return a "revise" verdict, causing post-critique.sh to re-trigger refine.yml with critique feedback as additional context. MAX_REVIEW_ROUNDS (default 3) prevents infinite loops. If the cap is reached, the plan is presented to a human for final decision. + +**Separate dispatch workflows**: The pipeline uses separate dispatch files (refine-dispatch.yml, jira-dispatch.yml) rather than integrating into the existing dispatch.yml. We chose this because it matches the validated pattern from fullsend-ai/features and avoids modifying the shared dispatch routing logic in Phase 1. + +**No eval system**: The features repo has a full eval system (MLflow baselines, fixture evals, eval-gate.yml). This is intentionally excluded from Phase 1 to reduce PR scope and review burden. + +### Deferred to Phase 2+ + +| Decision | Rationale for deferral | +|----------|----------------------| +| Consolidate dispatch into dispatch.yml | Blocked on #1985 (harness-driven dispatch). Once that lands, the separate dispatch files should be migrated. | +| Eval system integration | Complex (MLflow, baselines, scorer configs). Ship agents first, add eval infrastructure once we have production data to calibrate against. | +| Automatic triggers (e.g., trigger on issue creation with certain labels) | Phase 1 is deliberately manual-only to build confidence. Automatic triggers should be a separate ADR once we have usage data. | +| Single-workflow pipeline mode | Running all 3 stages in one workflow (no cross-run artifacts) would eliminate data leakage risk for public repos. Requires significant harness changes. | +| Dynamic role registration (#1985) | Currently adding roles to the hardcoded ValidRoles() list. Once #1985 lands, these should migrate to harness-declared roles. | + +## Consequences + +### Positive + +- Teams get automated issue decomposition from day one after installation +- The pipeline is entirely opt-in (requires explicit `/fs-refine` command) +- Chaining pattern is reusable for future multi-stage workflows +- Revision loop ensures quality without human intervention for routine work + +### Negative + +- Three new roles (each with its own GitHub App) increase the total agent count from 7 to 10 and add 3 apps to install +- Pipeline chaining is a new pattern that adds complexity to the scaffold +- Artifact-based data passing has a 90-day retention limit +- Separate dispatch files add workflow sprawl (mitigated by planned consolidation in Phase 2) + +### Risks + +- The revision loop could produce unnecessary iterations on simple issues (mitigated by confidence scoring and max rounds) +- Artifact downloads between workflow runs add latency (~30s per stage transition) +- Private Jira data could leak via public repo artifacts/logs (mitigated by a safety check that blocks Jira sources on public repos — see ADR 0050) diff --git a/docs/ADRs/0050-jira-integration-model.md b/docs/ADRs/0050-jira-integration-model.md new file mode 100644 index 000000000..ee4cd55eb --- /dev/null +++ b/docs/ADRs/0050-jira-integration-model.md @@ -0,0 +1,97 @@ +# ADR 0050: Jira Integration Model + +## Status + +Accepted + +## Context + +fullsend agents currently interact exclusively with GitHub Issues via the GitHub API. The refinement pipeline needs to work with Jira issues as well, since many teams use Jira as their primary issue tracker while hosting code on GitHub. + +Prior work on Jira integration (PR #2162 by Manish Kumar) validated the pattern of static API tokens with credential isolation, where Jira credentials are available to pre/post scripts on the trusted GitHub Actions runner but never enter the agent sandbox. That PR was closed without merging (review feedback required an ADR first), but the technical approach was validated on staging. + +This ADR covers the **Phase 1 Jira integration** — the minimum viable model to get the refinement pipeline working against Jira production. It intentionally defers several improvements to later phases. + +## Decision + +We will support Jira as an issue source for the refinement pipeline using the following model. + +### Phase 1 decisions (this PR) + +#### Authentication: Static API tokens + +- Jira access uses a service account email + API token stored as GitHub Actions secrets (`JIRA_HOST`, `JIRA_EMAIL`, `JIRA_API_TOKEN`) +- We chose static tokens because they work today, require zero infrastructure, and match what was validated in fullsend-ai/features +- **Credential isolation**: Jira credentials are available ONLY to pre/post scripts running on the trusted GitHub Actions runner. They are explicitly excluded from the agent sandbox environment. The agent cannot make Jira API calls directly. +- Pre-scripts fetch data and write it to `issue-context.json`; post-scripts read agent output and post results back to Jira + +#### Issue source routing + +Scripts use an `ISSUE_SOURCE` environment variable (`"github"` or `"jira"`) to determine: + +- Where to fetch issue data from (pre-scripts) +- Where to post results back to (post-scripts) +- What format to use (Markdown for GitHub, ADF for Jira via `markdown-to-adf.py`) + +#### Trigger mechanisms: Manual dispatch + comment poller + +For Phase 1, we ship two trigger mechanisms: + +1. **Manual dispatch**: Direct `gh workflow run jira-dispatch.yml -f jira_key=PROJ-123 -f command=/fs-refine`. This is the simplest path — zero Jira-side configuration needed. +2. **Jira comment poller** (cron, every 5 min): Polls Jira projects for issues labeled "fullsend" that have `/fs-refine` comments. Zero Jira admin access needed. Trade-off: GitHub Actions scheduled workflows run on a best-effort basis — the 5-minute cron is not guaranteed and can be delayed by minutes or skipped entirely during periods of high GitHub Actions load. This is acceptable for Phase 1 but is a key reason to prioritize Jira Automation Rule webhooks in Phase 2. + +Both mechanisms route through `jira-dispatch.yml`, which validates the issue key and dispatches the appropriate pipeline workflow. + +#### Security: Private Jira on public repos + +The risk is specific: **private** Jira data leaking into **public** GitHub Actions artifacts and logs. Public Jira projects running on public repos are fine — the data is already publicly accessible. + +The pipeline uses `JIRA_PROJECT_VISIBILITY` to control this. Teams set it as a **GitHub Actions repository variable** on their `.fullsend` config repo (Settings → Secrets and variables → Actions → Variables tab → `JIRA_PROJECT_VISIBILITY`). Values: + +- `"public"` — Jira project is publicly accessible, skip the check +- `"private"` or unset (default) — Jira project is private, require a private config repo + +When the value is `"private"` and the config repo is public, the pipeline **hard-fails** with a security error explaining the options. + +The variable flows through the full chain: scaffold shim (`vars.JIRA_PROJECT_VISIBILITY`) → reusable workflow (`jira_project_visibility` input) → agent environment → pre-script. The check is also enforced independently at `jira-dispatch.yml` and `jira-comment-poller.yml` before any agent is invoked. + +#### Data flow + +1. Pre-script fetches issue data from Jira REST API v3, normalizes to `issue-context.json` +2. Agent reads `issue-context.json` in sandbox (read-only) +3. Agent produces structured JSON output (explore/refine/critique result) +4. Post-script reads agent output, posts formatted comment back to Jira using ADF + +### Deferred to Phase 2+ + +| Decision | Why it's good | Why it's deferred | +|----------|--------------|-------------------| +| **Jira Automation Rule webhooks** | Instant trigger (no polling delay). A Jira Automation Rule fires a `repository_dispatch` event to GitHub when a comment containing `/fs-refine` is added. | Requires Jira admin access to create the rule per project. Phase 1 avoids any Jira-side setup requirements so teams can self-serve. The `jira-dispatch.yml` already handles `repository_dispatch` events — teams just need to create the Jira rule when ready. | +| **OAuth 2.0 with WIF for short-lived tokens** | Eliminates token rotation, follows ercohen's token mint proposal, aligns with fullsend's existing OIDC mint pattern. | Requires new infrastructure (token mint service for Jira). Static tokens work for initial adoption. Migrate when the token mint supports Jira. | +| **`fullsend jira enroll` CLI command** | Streamlines onboarding — one command sets up secrets, creates Jira Automation Rules, configures the poller. | Manish started this in PR #2162 but it wasn't merged. Needs design work to handle both per-org and per-repo modes. | +| **Jira custom field mapping for child issues** | Map additional Jira fields (story points, sprints, components, fix versions) when creating child issues. | Phase 1 creates children with summary, description, type, and parent link. Custom field mapping requires per-project configuration that varies across teams. | +| **Jira project discovery and enrollment** | Auto-detect which Jira projects to poll, manage project-to-repo mappings in config.yaml. | Phase 1 uses the "fullsend" label convention — poll all issues with that label. Explicit project enrollment adds config complexity that isn't needed for initial adoption. | +| **Private Jira on public repos** | Some teams have private Jira but public GitHub repos. A single-workflow pipeline mode or encrypted artifacts could enable this safely. | Architecturally complex. Phase 1 blocks this combination with a safety check. Teams in this situation should use a private config repo. | + +## Consequences + +### Positive + +- Jira credentials never touch the agent sandbox (security) +- Same agent prompts work for both GitHub and Jira issues +- Two trigger mechanisms cover the main use cases without requiring Jira admin access +- Public repo safety check prevents accidental data leakage +- The `jira-dispatch.yml` webhook handler is already built — enabling Jira Automation Rules in Phase 2 is zero code change + +### Negative + +- Static API tokens require manual rotation +- Cron poller delay is unpredictable — GitHub Actions scheduled workflows are best-effort and can be delayed or skipped under load (webhook trigger in Phase 2 eliminates this entirely) +- ADF (Atlassian Document Format) conversion adds complexity +- Child issues are created in both GitHub and Jira, with Jira hierarchy support (parent linking with retry-without-parent fallback) and automatic issue type resolution against the project's available types + +### Risks + +- Token rotation is a manual process — if a token expires, the pipeline silently fails until someone notices (mitigated: post-scripts log clear errors on auth failures) +- The poller creates GitHub Actions runs even when no commands are found (mitigated: the workflow exits quickly when there's nothing to process) +- GitHub's best-effort cron scheduling means the poller may not fire reliably, leading to user frustration if they expect timely responses (mitigated: manual dispatch is always available as a fallback, and Phase 2 Jira Automation webhooks provide instant, reliable triggering) diff --git a/docs/agents/README.md b/docs/agents/README.md index f8c074b56..8b091fb73 100644 --- a/docs/agents/README.md +++ b/docs/agents/README.md @@ -12,6 +12,9 @@ the YAML files in [`internal/scaffold/fullsend-repo/harness/`](../../internal/sc | [Review](review.md) | Reviews pull requests for correctness, security, and intent alignment | | [Fix](fix.md) | Addresses review feedback on open PRs | | [Retro](retro.md) | Analyzes completed workflows and proposes system improvements | +| [Explore](explore.md) | Investigates issues and gathers context for refinement | +| [Refine](refine.md) | Decomposes issues into structured implementation plans | +| [Critique](critique.md) | Reviews refinement plans and controls quality gates | ## Customization diff --git a/docs/agents/critique.md b/docs/agents/critique.md new file mode 100644 index 000000000..e4de1463f --- /dev/null +++ b/docs/agents/critique.md @@ -0,0 +1,69 @@ +# Critique Agent + +Critique agent icon + +Reviews the refinement plan for quality, completeness, and feasibility, then decides whether to approve it, request revisions, or escalate to a human. + +## How the agent works + +The critique agent receives the exploration context, the original issue, and the refinement plan produced by the [refine agent](refine.md). It evaluates the plan against six quality dimensions, cross-checks the refine agent's self-reported confidence, and scrutinizes assumptions that lack evidence from the exploration context. + +The agent runs in a read-only sandbox. It cannot modify issues, push code, or interact with external services. Its only output is a structured JSON verdict consumed by the post-script, which either creates child issues (on approval), re-triggers refine (on revise), or posts a question comment (on needs_input). + +Nothing gets created until this agent approves. + +## How it helps + +- Prevents over-decomposition (15 issues when 6 would suffice) from flooding the backlog. +- Catches vague children that restate the parent without adding specificity. +- Identifies missing coverage — entire dimensions of a feature forgotten by refine. +- Detects dependency cycles and impossible ordering. +- Guards against scope creep — children that exceed what the parent asked for. +- Catches "assumption laundering" — plans that look implementable but are built on unverified guesses. + +## Verdicts + +| Verdict | Meaning | What happens next | +|---------|---------|-------------------| +| `approved` | Plan is ready for child issue creation | Post-script creates issues (if `auto_create=true`) or posts approval comment | +| `revise` | Plan has fixable problems | Post-script re-triggers refine with critique feedback | +| `needs_input` | Human must answer a question before proceeding | Post-script posts a focused question as an issue comment | + +## Auto-create vs. human gate + +When the critique agent approves a plan: + +- **`auto_create=true`** (default): The post-script automatically creates child issues in the target tracker (GitHub or Jira). +- **`auto_create=false`**: The post-script posts an approval comment with the proposed plan. A human must comment `/fs-create` to trigger issue creation. + +This gives teams control over whether refinement is fully automated or requires human sign-off before backlog changes. + +## Review rounds + +- The critique agent tracks its review round via `REVIEW_ROUND` and has access to prior feedback via `CRITIQUE_HISTORY`. +- On re-reviews (round 2+), it focuses on whether specific requested revisions were addressed rather than re-evaluating the entire plan. +- Maximum review rounds default to 3 (configurable via `MAX_REVIEW_ROUNDS`). +- When close to the iteration limit, the threshold lowers — it approves with notes rather than forcing another round that would hit the cap. +- If the limit is reached without approval, the pipeline escalates to a human regardless of verdict. + +## Assessment dimensions + +| Dimension | What it checks | +|-----------|---------------| +| Coverage | Every requirement dimension has corresponding children | +| Granularity | Children are sized appropriately (epics = team-sized, stories = engineer-sized) | +| Dependency coherence | No cycles, achievable ordering, cross-team deps called out | +| Implementability | Engineers can read each child and know what to build | +| Scope accuracy | Children collectively match parent scope — no more, no less | +| Assumption grounding | Architectural decisions backed by evidence, not speculation | + +## Commands + +| Command | Where | Effect | +|---------|-------|--------| +| `/fs-refine` | Issue comment | Triggers the pipeline; critique runs automatically after refine | +| `/fs-create` | Issue comment | Creates child issues from an approved plan (when `auto_create=false`) | + +## Source + +[`internal/scaffold/fullsend-repo/harness/critique.yaml`](../../internal/scaffold/fullsend-repo/harness/critique.yaml) diff --git a/docs/agents/explore.md b/docs/agents/explore.md new file mode 100644 index 000000000..410032c1c --- /dev/null +++ b/docs/agents/explore.md @@ -0,0 +1,58 @@ +# Explore Agent + +Explore agent icon + +Investigates a GitHub or Jira issue to gather technical landscape, related work, architectural constraints, and competitive context before refinement begins. + +## How the agent works + +The explore agent is the first stage of the refinement pipeline, triggered when `/fs-refine` is invoked on an issue. It fetches the issue content — title, body, labels, comments, parent context — and then systematically researches the target codebase, related GitHub issues and PRs, Jira linked work, and the public web. + +The agent runs in a read-only sandbox. It cannot modify issues, push code, create PRs, or interact with external services beyond read access. Its only output is a structured JSON exploration context consumed by the downstream [refine agent](refine.md). + +The explore agent works with both GitHub Issues and Jira issues. When the issue source is Jira, the pre-script fetches issue data via the Jira REST API and normalizes it to `issue-context.json` before the agent starts. + +## How it helps + +- Gathers technical context that would take a human hours to assemble — project structure, dependencies, deployment targets, API surface, test infrastructure. +- Identifies related work (prior issues, PRs, abandoned attempts) so the refine agent doesn't propose work that already exists or was previously rejected. +- Surfaces architectural constraints and competitive approaches that inform decomposition decisions. +- Assesses confidence across five dimensions, flagging specific gaps so the refine agent knows where it lacks grounding. + +## Commands + +| Command | Where | Effect | +|---------|-------|--------| +| `/fs-refine` | Issue comment | Triggers the refinement pipeline starting with explore | + +The `/fs-refine` command kicks off the full pipeline. Explore runs first, then automatically chains to [refine](refine.md). There is no separate command to run explore in isolation. + +For Jira issues, the pipeline can also be triggered by: +- A Jira Automation webhook sending a `repository_dispatch` event +- The Jira comment poller (cron job, every 5 minutes) detecting `/fs-refine` comments + +## Pipeline flow + +``` +/fs-refine → explore → refine → critique → [approved | revise | needs_input] +``` + +Explore produces `exploration_context.json` as a GitHub Actions artifact. The post-script chains to the refine workflow via `workflow_dispatch`, passing the run ID for artifact correlation. + +## Exploration dimensions + +The agent assesses confidence (0–100) across these dimensions: + +| Dimension | What it measures | +|-----------|-----------------| +| `technical_landscape` | Codebase, APIs, and patterns understood | +| `related_work` | Prior issues, PRs, and discussions found | +| `architectural_constraints` | Deployment targets, dependencies, and contracts | +| `competitive_context` | How alternatives handle this problem | +| `requirements_clarity` | Whether the work item is clear enough to decompose | + +Dimensions scoring below 60 are flagged with specific gap descriptions in the output. + +## Source + +[`internal/scaffold/fullsend-repo/harness/explore.yaml`](../../internal/scaffold/fullsend-repo/harness/explore.yaml) diff --git a/docs/agents/refine.md b/docs/agents/refine.md new file mode 100644 index 000000000..6646e801a --- /dev/null +++ b/docs/agents/refine.md @@ -0,0 +1,60 @@ +# Refine Agent + +Refine agent icon + +Takes exploration context and decomposes a work item into a structured plan of child issues — epics, stories, and tasks — with acceptance criteria, dependencies, and effort estimates. + +## How the agent works + +The refine agent receives the exploration context produced by the [explore agent](explore.md) and the original issue content. It reads the target codebase, assesses its confidence across multiple dimensions, and produces a complete hierarchical decomposition of the work item into implementable child issues. + +The agent runs in a read-only sandbox. It cannot modify issues, push code, or interact with external services. Its only output is a structured JSON refinement plan consumed by the downstream [critique agent](critique.md). + +The refine agent always produces a plan — it never halts to ask questions. When information is incomplete, it makes its best judgment, flags assumptions explicitly, and adds open questions for the critique agent to evaluate. + +## How it helps + +- Turns vague features into actionable work items with testable acceptance criteria. +- Produces full decomposition trees (feature → epics → stories → tasks) in a single pass. +- Names specific APIs, tools, libraries, and configuration patterns — not vague capability references. +- Identifies cross-team dependencies and blocking relationships. +- Reduces manual refinement time from hours of team discussion to minutes of automated analysis. + +## Commands + +| Command | Where | Effect | +|---------|-------|--------| +| `/fs-refine` | Issue comment | Triggers the pipeline; refine runs automatically after explore | + +There is no separate command to invoke refine alone. It runs as the second stage of the refinement pipeline after explore completes. + +## Pipeline flow + +``` +/fs-refine → explore → refine → critique → [approved | revise | needs_input] + ↑ | + └── revision feedback ─────┘ +``` + +When the [critique agent](critique.md) returns a `revise` verdict, the pipeline re-triggers refine with the critique feedback as additional context. The refine agent must address each requested revision or explain why it chose a different approach. + +## Revision loop + +- Critique can send work back to refine with specific, actionable revisions. +- Each revision has a type: `remove`, `merge`, `split`, `revise`, or `add`. +- The refine agent incorporates feedback and produces an updated plan. +- Maximum review rounds default to 3 (configurable via `MAX_REVIEW_ROUNDS`). +- If the limit is reached without approval, the pipeline escalates to a human. + +## Output structure + +The refinement plan includes: +- Hierarchical children using `parent_title` for tree structure +- Confidence scores per child and per dimension +- Open questions and uncited assumptions (for critique to evaluate) +- A proposed enhanced feature description +- A summary comment for the issue + +## Source + +[`internal/scaffold/fullsend-repo/harness/refine.yaml`](../../internal/scaffold/fullsend-repo/harness/refine.yaml) diff --git a/internal/config/config.go b/internal/config/config.go index 6dcf4897e..d8ba9c36a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -91,7 +91,7 @@ type OrgConfig struct { // ValidRoles returns the set of recognized agent roles. func ValidRoles() []string { - return []string{"fullsend", "triage", "coder", "review", "fix", "retro", "prioritize"} + return []string{"fullsend", "triage", "coder", "review", "fix", "retro", "prioritize", "explore", "refine", "critique"} } // ValidProviders returns the set of recognized inference providers. @@ -103,14 +103,14 @@ func ValidProviders() []string { // when no custom roles are specified. The fix stage reuses the coder // app (role: coder) so it does not need a separate app or PEM. func DefaultAgentRoles() []string { - return []string{"fullsend", "triage", "coder", "review", "retro", "prioritize"} + return []string{"fullsend", "triage", "coder", "review", "retro", "prioritize", "explore", "refine", "critique"} } // PerRepoDefaultRoles returns agent roles for per-repo installation. // The "fullsend" dispatch role is excluded because per-repo mode uses // the target repo's shim workflow for dispatch instead of a separate app. func PerRepoDefaultRoles() []string { - return []string{"triage", "coder", "review", "fix", "retro", "prioritize"} + return []string{"triage", "coder", "review", "fix", "retro", "prioritize", "explore", "refine", "critique"} } // NewOrgConfig creates a new OrgConfig with sensible defaults. diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a9ce98b57..fdbb4bcc6 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -10,7 +10,7 @@ import ( func TestValidRoles(t *testing.T) { roles := ValidRoles() - assert.Len(t, roles, 7) + assert.Len(t, roles, 10) assert.Contains(t, roles, "fullsend") assert.Contains(t, roles, "triage") assert.Contains(t, roles, "coder") @@ -18,17 +18,23 @@ func TestValidRoles(t *testing.T) { assert.Contains(t, roles, "fix") assert.Contains(t, roles, "retro") assert.Contains(t, roles, "prioritize") + assert.Contains(t, roles, "explore") + assert.Contains(t, roles, "refine") + assert.Contains(t, roles, "critique") } func TestPerRepoDefaultRoles(t *testing.T) { roles := PerRepoDefaultRoles() - assert.Len(t, roles, 6) + assert.Len(t, roles, 9) assert.Contains(t, roles, "triage") assert.Contains(t, roles, "coder") assert.Contains(t, roles, "review") assert.Contains(t, roles, "fix") assert.Contains(t, roles, "retro") assert.Contains(t, roles, "prioritize") + assert.Contains(t, roles, "explore") + assert.Contains(t, roles, "refine") + assert.Contains(t, roles, "critique") // "fullsend" dispatch role must be excluded in per-repo mode. assert.NotContains(t, roles, "fullsend") } diff --git a/internal/forge/github/types.go b/internal/forge/github/types.go index 6d0354935..46a3851e7 100644 --- a/internal/forge/github/types.go +++ b/internal/forge/github/types.go @@ -38,7 +38,7 @@ type AppConfig struct { // DefaultAgentRoles returns the standard set of agent roles. func DefaultAgentRoles() []string { - return []string{"fullsend", "triage", "coder", "review", "retro", "prioritize"} + return []string{"fullsend", "triage", "coder", "review", "retro", "prioritize", "explore", "refine", "critique"} } // AgentAppConfig returns the GitHub App configuration for a given agent role. @@ -139,6 +139,33 @@ func AgentAppConfig(org, role, appSet string) AppConfig { // No webhook events — triggered via workflow_dispatch from other agents. base.Events = []string{} + case "explore": + base.Description = fmt.Sprintf("Fullsend explore agent for %s", org) + base.Permissions = AppPermissions{ + Actions: "write", + Contents: "read", + Issues: "write", + } + base.Events = []string{} + + case "refine": + base.Description = fmt.Sprintf("Fullsend refine agent for %s", org) + base.Permissions = AppPermissions{ + Actions: "write", + Contents: "read", + Issues: "write", + } + base.Events = []string{} + + case "critique": + base.Description = fmt.Sprintf("Fullsend critique agent for %s", org) + base.Permissions = AppPermissions{ + Actions: "write", + Contents: "read", + Issues: "write", + } + base.Events = []string{} + default: base.Description = fmt.Sprintf("Fullsend %s agent for %s", role, org) base.Permissions = AppPermissions{ diff --git a/internal/forge/github/types_test.go b/internal/forge/github/types_test.go index 097191002..eceb370b4 100644 --- a/internal/forge/github/types_test.go +++ b/internal/forge/github/types_test.go @@ -10,8 +10,8 @@ import ( func TestDefaultAgentRoles(t *testing.T) { roles := DefaultAgentRoles() - require.Len(t, roles, 6) - assert.Equal(t, []string{"fullsend", "triage", "coder", "review", "retro", "prioritize"}, roles) + require.Len(t, roles, 9) + assert.Equal(t, []string{"fullsend", "triage", "coder", "review", "retro", "prioritize", "explore", "refine", "critique"}, roles) } func TestAgentAppConfig_Fullsend(t *testing.T) { @@ -102,6 +102,42 @@ func TestAgentAppConfig_Retro(t *testing.T) { assert.Empty(t, cfg.Events) } +func TestAgentAppConfig_Explore(t *testing.T) { + cfg := AgentAppConfig("myorg", "explore", "fullsend") + + assert.Equal(t, "fullsend-explore", cfg.Name) + assert.Equal(t, "write", cfg.Permissions.Actions) + assert.Equal(t, "read", cfg.Permissions.Contents) + assert.Equal(t, "write", cfg.Permissions.Issues) + assert.Empty(t, cfg.Permissions.PullRequests) + + assert.Empty(t, cfg.Events) +} + +func TestAgentAppConfig_Refine(t *testing.T) { + cfg := AgentAppConfig("myorg", "refine", "fullsend") + + assert.Equal(t, "fullsend-refine", cfg.Name) + assert.Equal(t, "write", cfg.Permissions.Actions) + assert.Equal(t, "read", cfg.Permissions.Contents) + assert.Equal(t, "write", cfg.Permissions.Issues) + assert.Empty(t, cfg.Permissions.PullRequests) + + assert.Empty(t, cfg.Events) +} + +func TestAgentAppConfig_Critique(t *testing.T) { + cfg := AgentAppConfig("myorg", "critique", "fullsend") + + assert.Equal(t, "fullsend-critique", cfg.Name) + assert.Equal(t, "write", cfg.Permissions.Actions) + assert.Equal(t, "read", cfg.Permissions.Contents) + assert.Equal(t, "write", cfg.Permissions.Issues) + assert.Empty(t, cfg.Permissions.PullRequests) + + assert.Empty(t, cfg.Events) +} + func TestAgentAppConfig_UnknownRole(t *testing.T) { cfg := AgentAppConfig("myorg", "custom-bot", "fullsend") diff --git a/internal/harness/scaffold_integration_test.go b/internal/harness/scaffold_integration_test.go index 519355f03..4a8a5fa96 100644 --- a/internal/harness/scaffold_integration_test.go +++ b/internal/harness/scaffold_integration_test.go @@ -177,8 +177,8 @@ func TestDiscoverAgents_ScaffoldDirectory(t *testing.T) { agents, err := DiscoverAgents(harnessDir) require.NoError(t, err) - // Expect all 6 scaffold harnesses discovered. - require.Len(t, agents, 6, "should discover all 6 scaffold harnesses") + // Expect all 9 scaffold harnesses discovered. + require.Len(t, agents, 9, "should discover all 9 scaffold harnesses") // Build a map of filename -> AgentInfo for easier assertion. byFilename := make(map[string]AgentInfo, len(agents)) @@ -193,6 +193,9 @@ func TestDiscoverAgents_ScaffoldDirectory(t *testing.T) { "retro.yaml": {"retro", "fullsend-ai-retro"}, "review.yaml": {"review", "fullsend-ai-review"}, "triage.yaml": {"triage", "fullsend-ai-triage"}, + "explore.yaml": {"explore", "fullsend-ai-explore"}, + "refine.yaml": {"refine", "fullsend-ai-refine"}, + "critique.yaml": {"critique", "fullsend-ai-critique"}, } for filename, want := range expected { diff --git a/internal/scaffold/baseurl_test.go b/internal/scaffold/baseurl_test.go index 7e348f929..c854e3353 100644 --- a/internal/scaffold/baseurl_test.go +++ b/internal/scaffold/baseurl_test.go @@ -157,7 +157,7 @@ func TestHarnessNames(t *testing.T) { require.NoError(t, err) t.Run("returns expected harnesses", func(t *testing.T) { - expected := []string{"code", "fix", "prioritize", "retro", "review", "triage"} + expected := []string{"code", "critique", "explore", "fix", "prioritize", "refine", "retro", "review", "triage"} assert.Equal(t, expected, names) }) diff --git a/internal/scaffold/fullsend-repo/.github/workflows/create-children.yml b/internal/scaffold/fullsend-repo/.github/workflows/create-children.yml new file mode 100644 index 000000000..e0d98e65f --- /dev/null +++ b/internal/scaffold/fullsend-repo/.github/workflows/create-children.yml @@ -0,0 +1,60 @@ +--- +# fullsend-stage: create-children +name: Create Children + +permissions: + actions: write + contents: read + id-token: write + issues: write + +on: + workflow_dispatch: + inputs: + issue_key: + required: true + type: string + issue_source: + required: false + type: string + default: "github" + refine_run_id: + required: true + type: string + event_type: + required: false + type: string + default: "workflow_dispatch" + source_repo: + required: false + type: string + default: "" + event_payload: + required: false + type: string + default: "{}" + +concurrency: + group: fullsend-create-children-${{ inputs.issue_key || 'unknown' }} + cancel-in-progress: false + +jobs: + create-children: + uses: __REUSABLE_WORKFLOW__ + with: + event_type: ${{ inputs.event_type || 'workflow_dispatch' }} + source_repo: ${{ inputs.source_repo || github.repository }} + event_payload: ${{ inputs.event_payload || '{}' }} + mint_url: ${{ vars.FULLSEND_MINT_URL }} + gcp_region: ${{ vars.FULLSEND_GCP_REGION }} + install_mode: per-org + fullsend_ai_ref: v0 + issue_key: ${{ inputs.issue_key }} + issue_source: ${{ inputs.issue_source }} + refine_run_id: ${{ inputs.refine_run_id }} + secrets: + FULLSEND_GCP_WIF_PROVIDER: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }} + FULLSEND_GCP_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }} + JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} diff --git a/internal/scaffold/fullsend-repo/.github/workflows/critique.yml b/internal/scaffold/fullsend-repo/.github/workflows/critique.yml new file mode 100644 index 000000000..dd758183c --- /dev/null +++ b/internal/scaffold/fullsend-repo/.github/workflows/critique.yml @@ -0,0 +1,64 @@ +--- +# fullsend-stage: critique +name: Critique + +permissions: + actions: write + contents: read + id-token: write + issues: write + +on: + workflow_dispatch: + inputs: + issue_key: + required: true + type: string + issue_source: + required: false + type: string + default: "github" + refine_run_id: + required: true + type: string + review_round: + required: false + type: string + default: "1" + max_review_rounds: + required: false + type: string + default: "3" + auto_create: + required: false + type: string + default: "false" + +concurrency: + group: fullsend-critique-${{ inputs.issue_key || 'unknown' }} + cancel-in-progress: false + +jobs: + critique: + uses: __REUSABLE_WORKFLOW__ + with: + event_type: workflow_dispatch + source_repo: ${{ github.repository }} + event_payload: "{}" + mint_url: ${{ vars.FULLSEND_MINT_URL }} + gcp_region: ${{ vars.FULLSEND_GCP_REGION }} + install_mode: per-org + fullsend_ai_ref: v0 + issue_key: ${{ inputs.issue_key }} + issue_source: ${{ inputs.issue_source }} + refine_run_id: ${{ inputs.refine_run_id }} + review_round: ${{ inputs.review_round }} + max_review_rounds: ${{ inputs.max_review_rounds }} + auto_create: ${{ inputs.auto_create }} + jira_project_visibility: ${{ vars.JIRA_PROJECT_VISIBILITY || 'private' }} + secrets: + FULLSEND_GCP_WIF_PROVIDER: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }} + FULLSEND_GCP_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }} + JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} diff --git a/internal/scaffold/fullsend-repo/.github/workflows/explore.yml b/internal/scaffold/fullsend-repo/.github/workflows/explore.yml new file mode 100644 index 000000000..cf26ed005 --- /dev/null +++ b/internal/scaffold/fullsend-repo/.github/workflows/explore.yml @@ -0,0 +1,65 @@ +--- +# fullsend-stage: explore +name: Explore + +permissions: + actions: write + contents: read + id-token: write + issues: write + +on: + workflow_dispatch: + inputs: + issue_key: + required: true + type: string + issue_source: + required: false + type: string + default: "github" + event_type: + required: false + type: string + default: "workflow_dispatch" + source_repo: + required: false + type: string + default: "" + event_payload: + required: false + type: string + default: "{}" + auto_create: + required: false + type: string + default: "false" + repo_full_name: + required: false + type: string + default: "" + +concurrency: + group: fullsend-explore-${{ inputs.issue_key || 'unknown' }} + cancel-in-progress: true + +jobs: + explore: + uses: __REUSABLE_WORKFLOW__ + with: + event_type: ${{ inputs.event_type || 'workflow_dispatch' }} + source_repo: ${{ inputs.source_repo || github.repository }} + event_payload: ${{ inputs.event_payload || '{}' }} + mint_url: ${{ vars.FULLSEND_MINT_URL }} + gcp_region: ${{ vars.FULLSEND_GCP_REGION }} + install_mode: per-org + fullsend_ai_ref: v0 + issue_key: ${{ inputs.issue_key }} + issue_source: ${{ inputs.issue_source }} + jira_project_visibility: ${{ vars.JIRA_PROJECT_VISIBILITY || 'private' }} + secrets: + FULLSEND_GCP_WIF_PROVIDER: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }} + FULLSEND_GCP_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }} + JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} diff --git a/internal/scaffold/fullsend-repo/.github/workflows/jira-comment-poller.yml b/internal/scaffold/fullsend-repo/.github/workflows/jira-comment-poller.yml new file mode 100644 index 000000000..4f9438329 --- /dev/null +++ b/internal/scaffold/fullsend-repo/.github/workflows/jira-comment-poller.yml @@ -0,0 +1,149 @@ +--- +# fullsend-stage: jira-comment-poller +name: jira-comment-poller + +permissions: + actions: write + contents: read + +on: + schedule: + - cron: '*/5 * * * *' + workflow_dispatch: + +concurrency: + group: jira-comment-poller + cancel-in-progress: true + +jobs: + poll: + runs-on: ubuntu-latest + steps: + - name: Poll Jira for /fs-* commands + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + REPO: ${{ github.repository }} + JIRA_PROJECT_VISIBILITY: ${{ vars.JIRA_PROJECT_VISIBILITY }} + run: | + set -euo pipefail + + if [[ -z "${JIRA_HOST:-}" || -z "${JIRA_EMAIL:-}" || -z "${JIRA_API_TOKEN:-}" ]]; then + echo "::error::JIRA_HOST, JIRA_EMAIL, and JIRA_API_TOKEN secrets are required" + exit 1 + fi + + # Block private Jira on public repos to prevent data leakage. + # Public Jira projects on public repos are fine — set JIRA_PROJECT_VISIBILITY=public. + JIRA_VIS="${JIRA_PROJECT_VISIBILITY:-private}" + if [[ "$JIRA_VIS" != "public" ]]; then + REPO_VISIBILITY=$(gh api "repos/${REPO}" --jq '.visibility' 2>/dev/null || echo "unknown") + if [[ "$REPO_VISIBILITY" == "public" ]]; then + echo "::error::SECURITY: Private Jira poller blocked — repo ${REPO} is public." + echo "::error::Private Jira data would leak into public artifacts/logs." + echo "::error::Use a private config repo, or set JIRA_PROJECT_VISIBILITY=public for public Jira projects." + exit 1 + fi + fi + + AUTH=$(printf '%s:%s' "$JIRA_EMAIL" "$JIRA_API_TOKEN" | base64 -w0) + ACK_MARKER="⚡ fullsend" + + jira_api() { + local method="$1" endpoint="$2" + shift 2 + curl -sSf -X "$method" \ + -H "Authorization: Basic $AUTH" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + "https://${JIRA_HOST}${endpoint}" "$@" + } + + echo "Searching for fullsend-labeled issues..." + ISSUES=$(jira_api POST '/rest/api/3/search/jql' \ + -d '{"jql":"labels = fullsend AND updated >= -1d","fields":["key","summary"],"maxResults":50}') + ISSUE_COUNT=$(echo "$ISSUES" | jq '.issues | length') + echo "Found $ISSUE_COUNT issue(s)" + + if [[ "$ISSUE_COUNT" == "0" ]]; then + echo "No issues to process" + exit 0 + fi + + DISPATCHED=0 + + echo "$ISSUES" | jq -r '.issues[].key' | while read -r ISSUE_KEY; do + echo "" + echo "--- Checking $ISSUE_KEY ---" + + COMMENTS=$(jira_api GET "/rest/api/3/issue/${ISSUE_KEY}/comment?orderBy=-created&maxResults=20") + + echo "$COMMENTS" | jq -r '.comments[] | @base64' | while read -r COMMENT_B64; do + COMMENT_ID=$(echo "$COMMENT_B64" | base64 -d | jq -r '.id') + AUTHOR=$(echo "$COMMENT_B64" | base64 -d | jq -r '.author.displayName // .author.emailAddress // "unknown"') + CREATED=$(echo "$COMMENT_B64" | base64 -d | jq -r '.created') + + BODY=$(echo "$COMMENT_B64" | base64 -d | jq -r ' + [.body.content[]? | .. | .text? // empty] | join(" ") + ') + + if printf '%s' "$BODY" | grep -qF "$ACK_MARKER"; then + continue + fi + + if [[ "$AUTHOR" == "Fullsend Refinement Squad" ]]; then + continue + fi + + if ! printf '%s' "$BODY" | grep -qP '(?:^|\s)/fs-\S+'; then + continue + fi + + echo " Comment $COMMENT_ID by $AUTHOR at $CREATED" + echo " Body: $BODY" + + ALREADY_ACKED=$(echo "$COMMENTS" | jq -r --arg cid "$COMMENT_ID" --arg marker "$ACK_MARKER" ' + [.comments[] | + select(.id != $cid) | + select( + [.body.content[]? | .. | .text? // empty] | join(" ") | + contains($marker) and contains($cid) + ) + ] | length + ') + + if [[ "$ALREADY_ACKED" != "0" ]]; then + echo " → Already processed (ack found), skipping" + continue + fi + + COMMAND_LINE=$(printf '%s' "$BODY" | grep -oP '(?:^|\s)\K/fs-\w+(?:\s+--\w[\w-]*(?:\s+\S+))*' | head -1) + echo " → Dispatching: $COMMAND_LINE" + + gh workflow run jira-dispatch.yml \ + --repo "$REPO" \ + -f jira_key="$ISSUE_KEY" \ + -f command="$COMMAND_LINE" || { + echo " → ERROR: dispatch failed" + continue + } + + ACK_TEXT="${ACK_MARKER} dispatched ${COMMAND_LINE} → GitHub Actions (comment:${COMMENT_ID})" + ACK_BODY=$(jq -n --arg text "$ACK_TEXT" '{ + body: {type:"doc",version:1,content:[{ + type:"paragraph", + content:[{type:"text",text:$text,marks:[{type:"strong"}]}] + }]} + }') + + jira_api POST "/rest/api/3/issue/${ISSUE_KEY}/comment" -d "$ACK_BODY" > /dev/null + + echo " → Dispatched and acknowledged" + DISPATCHED=$((DISPATCHED + 1)) + done + done + + echo "" + echo "=== Polling complete ===" diff --git a/internal/scaffold/fullsend-repo/.github/workflows/jira-dispatch.yml b/internal/scaffold/fullsend-repo/.github/workflows/jira-dispatch.yml new file mode 100644 index 000000000..1a95f6418 --- /dev/null +++ b/internal/scaffold/fullsend-repo/.github/workflows/jira-dispatch.yml @@ -0,0 +1,185 @@ +--- +# fullsend-stage: jira-dispatch +name: jira-dispatch + +permissions: + actions: write + contents: read + id-token: write + issues: write + +on: + workflow_dispatch: + inputs: + jira_key: + description: 'Jira issue key (e.g., SECURESIGN-1251)' + required: true + type: string + command: + description: 'Slash command to execute (e.g., /fs-refine, /fs-create)' + required: true + type: string + default: '/fs-refine' + repository_dispatch: + types: [jira-command, jira-refine] + +concurrency: + group: jira-dispatch-${{ inputs.jira_key || github.event.client_payload.jira_key || 'unknown' }} + cancel-in-progress: false + +jobs: + dispatch: + runs-on: ubuntu-latest + steps: + - name: Parse command and dispatch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + JIRA_KEY: ${{ inputs.jira_key || github.event.client_payload.jira_key }} + RAW_COMMAND: ${{ inputs.command || github.event.client_payload.command || '' }} + EVENT_TYPE: ${{ github.event.action || 'workflow_dispatch' }} + REPO: ${{ github.repository }} + JIRA_PROJECT_VISIBILITY: ${{ vars.JIRA_PROJECT_VISIBILITY }} + run: | + set -euo pipefail + + if [[ -z "$JIRA_KEY" ]]; then + echo "::error::jira_key is required" + exit 1 + fi + + if [[ ! "$JIRA_KEY" =~ ^[A-Z][A-Z0-9]+-[0-9]+$ ]]; then + echo "::error::jira_key must match PROJECT-123 pattern, got: $JIRA_KEY" + exit 1 + fi + + # Block private Jira on public repos to prevent data leakage. + # Public Jira projects on public repos are fine — set JIRA_PROJECT_VISIBILITY=public. + JIRA_VIS="${JIRA_PROJECT_VISIBILITY:-private}" + if [[ "$JIRA_VIS" != "public" ]]; then + REPO_VISIBILITY=$(gh api "repos/${REPO}" --jq '.visibility' 2>/dev/null || echo "unknown") + if [[ "$REPO_VISIBILITY" == "public" ]]; then + echo "::error::SECURITY: Private Jira dispatch blocked — repo ${REPO} is public." + echo "::error::Private Jira data would leak into public artifacts/logs." + echo "::error::Use a private config repo, or set JIRA_PROJECT_VISIBILITY=public for public Jira projects." + exit 1 + fi + fi + + if [[ "$EVENT_TYPE" == "jira-refine" && -z "$RAW_COMMAND" ]]; then + RAW_COMMAND="/fs-refine" + fi + + COMMAND="$(printf '%s\n' "$RAW_COMMAND" | head -1 | awk '{print $1}')" + + if [[ -z "$COMMAND" ]]; then + echo "::error::No command found in payload" + exit 1 + fi + + echo "Jira dispatch:" + echo " Jira key: ${JIRA_KEY}" + echo " Command: ${COMMAND}" + echo " Full command: ${RAW_COMMAND}" + echo " Event type: ${EVENT_TYPE}" + + case "$COMMAND" in + + /fs-refine) + SKIP_EXPLORE=false + CONTEXT_REF="" + AUTO_CREATE="false" + TARGET_REPO="" + + if printf '%s\n' "$RAW_COMMAND" | grep -qw -- '--skip-explore'; then + SKIP_EXPLORE=true + fi + if printf '%s\n' "$RAW_COMMAND" | grep -qw -- '--auto-create'; then + AUTO_CREATE="true" + fi + CONTEXT_REF=$(printf '%s\n' "$RAW_COMMAND" | grep -oP '(?<=--context\s)\S+' || true) + TARGET_REPO=$(printf '%s\n' "$RAW_COMMAND" | grep -oP '(?<=--repo\s)\S+' || true) + + echo " Skip explore: ${SKIP_EXPLORE}" + echo " Auto-create: ${AUTO_CREATE}" + echo " Context ref: ${CONTEXT_REF:-none}" + echo " Target repo: ${TARGET_REPO:-none (web research only)}" + + DISPATCH_ARGS=( + -f issue_key="$JIRA_KEY" + -f issue_source="jira" + -f auto_create="$AUTO_CREATE" + ) + + if [[ -n "$TARGET_REPO" ]]; then + DISPATCH_ARGS+=(-f repo_full_name="$TARGET_REPO") + fi + + if [[ "$SKIP_EXPLORE" == "true" || -n "$CONTEXT_REF" ]]; then + if [[ -n "$CONTEXT_REF" ]]; then + DISPATCH_ARGS+=(-f explore_context_ref="$CONTEXT_REF") + fi + gh workflow run refine.yml \ + --repo "$REPO" \ + "${DISPATCH_ARGS[@]}" + + echo "::notice::Refine dispatched directly for ${JIRA_KEY} (skip explore)" + else + gh workflow run explore.yml \ + --repo "$REPO" \ + "${DISPATCH_ARGS[@]}" + + echo "::notice::Explore dispatched for ${JIRA_KEY}${TARGET_REPO:+ (target: $TARGET_REPO)}" + fi + ;; + + /fs-create) + echo "Dispatching child issue creation for ${JIRA_KEY}" + + EXISTING_CREATE=$(gh run list --repo "$REPO" --workflow "create-children" \ + --limit 10 --json databaseId,status,conclusion,displayTitle \ + --jq "[.[] | select(.status == \"completed\" and .conclusion == \"success\" and (.displayTitle | contains(\"$JIRA_KEY\")))] | .[0].databaseId // empty" \ + 2>/dev/null || true) + + if [[ -n "$EXISTING_CREATE" ]]; then + echo "::error::Child issues were already created for ${JIRA_KEY} (run: ${EXISTING_CREATE}). To recreate, run /fs-refine first." + exit 1 + fi + + REFINE_RUN_ID=$(gh run list --repo "$REPO" --workflow "fullsend-refine" \ + --limit 20 --json databaseId,status,conclusion,displayTitle \ + --jq "[.[] | select(.status == \"completed\" and .conclusion == \"success\" and (.displayTitle | contains(\"$JIRA_KEY\")))] | .[0].databaseId // empty" \ + 2>/dev/null || true) + + if [[ -z "$REFINE_RUN_ID" ]]; then + echo "::error::No successful refine run found to create issues from" + exit 1 + fi + + CRITIQUE_RUN_ID=$(gh run list --repo "$REPO" --workflow "fullsend-critique" \ + --limit 10 --json databaseId,displayTitle \ + --jq "[.[] | select(.displayTitle | contains(\"$JIRA_KEY\"))] | .[0].databaseId // empty" \ + 2>/dev/null || true) + + DISPATCH_ARGS=( + --repo "$REPO" + -f issue_key="$JIRA_KEY" + -f issue_source="jira" + -f repo_full_name="$REPO" + -f refine_run_id="$REFINE_RUN_ID" + ) + + if [[ -n "$CRITIQUE_RUN_ID" ]]; then + DISPATCH_ARGS+=(-f critique_run_id="$CRITIQUE_RUN_ID") + echo "Found critique run for ${JIRA_KEY}: $CRITIQUE_RUN_ID" + fi + + gh workflow run create-children.yml "${DISPATCH_ARGS[@]}" + + echo "::notice::Child issue creation dispatched for ${JIRA_KEY} (refine run: ${REFINE_RUN_ID}${CRITIQUE_RUN_ID:+, critique run: $CRITIQUE_RUN_ID})" + ;; + + *) + echo "::error::Unknown command '${COMMAND}'. Supported: /fs-refine, /fs-create" + exit 1 + ;; + esac diff --git a/internal/scaffold/fullsend-repo/.github/workflows/refine-dispatch.yml b/internal/scaffold/fullsend-repo/.github/workflows/refine-dispatch.yml new file mode 100644 index 000000000..a2d38cf2a --- /dev/null +++ b/internal/scaffold/fullsend-repo/.github/workflows/refine-dispatch.yml @@ -0,0 +1,200 @@ +--- +# fullsend-stage: refine-dispatch +name: refine-dispatch + +permissions: + actions: write + contents: read + issues: write + +on: + issue_comment: + types: [created] + +jobs: + dispatch: + if: >- + github.event.comment.user.type != 'Bot' + && ( + github.event.comment.author_association == 'OWNER' + || github.event.comment.author_association == 'MEMBER' + || github.event.comment.author_association == 'COLLABORATOR' + || github.event.comment.user.login == github.event.issue.user.login + ) + && ( + startsWith(github.event.comment.body, '/fs-refine') + || startsWith(github.event.comment.body, '/fs-create') + || ( + contains(join(github.event.issue.labels.*.name, ','), 'refine-needs-input') + ) + || ( + contains(join(github.event.issue.labels.*.name, ','), 'human-refinement') + ) + ) + runs-on: ubuntu-latest + steps: + - name: Parse command and dispatch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMMENT_BODY: ${{ github.event.comment.body }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPO: ${{ github.repository }} + LABELS: ${{ join(github.event.issue.labels.*.name, ',') }} + run: | + set -euo pipefail + + COMMAND="$(printf '%s\n' "${COMMENT_BODY}" | head -1 | awk '{print $1}')" + IS_REFINE_CMD=false + IS_CREATE_CMD=false + IS_FOLLOWUP=false + + if [[ "$COMMAND" == "/fs-refine" ]]; then + IS_REFINE_CMD=true + elif [[ "$COMMAND" == "/fs-create" ]]; then + IS_CREATE_CMD=true + elif echo "$LABELS" | grep -qF "refine-needs-input" || echo "$LABELS" | grep -qF "human-refinement"; then + IS_FOLLOWUP=true + fi + + if [[ "$IS_REFINE_CMD" == "false" && "$IS_CREATE_CMD" == "false" && "$IS_FOLLOWUP" == "false" ]]; then + echo "Not a refine/create trigger — skipping" + exit 0 + fi + + if [[ "$IS_CREATE_CMD" == "true" ]]; then + echo "Dispatching child issue creation for #${ISSUE_NUMBER}" + + if ! echo "$LABELS" | grep -qF "refine-approved"; then + gh api "repos/${REPO}/issues/comments/${{ github.event.comment.id }}/reactions" \ + -f content="confused" --silent 2>/dev/null || true + echo "::error::Cannot create children — the plan has not been approved by the critique agent (missing 'refine-approved' label)" + exit 1 + fi + + REFINE_RUN_ID=$(gh run list --repo "$REPO" --workflow "fullsend-refine" \ + --limit 20 --json databaseId,status,conclusion,displayTitle \ + --jq "[.[] | select(.status == \"completed\" and .conclusion == \"success\" and (.displayTitle | endswith(\" · ${ISSUE_NUMBER}\")))] | .[0].databaseId // empty" \ + 2>/dev/null || true) + + if [[ -z "$REFINE_RUN_ID" ]]; then + gh api "repos/${REPO}/issues/comments/${{ github.event.comment.id }}/reactions" \ + -f content="confused" --silent 2>/dev/null || true + echo "::error::No successful refine run found for issue #${ISSUE_NUMBER}" + exit 1 + fi + + CRITIQUE_RUN_ID=$(gh run list --repo "$REPO" --workflow "fullsend-critique" \ + --limit 10 --json databaseId,displayTitle \ + --jq "[.[] | select(.displayTitle | endswith(\" · ${ISSUE_NUMBER}\"))] | .[0].databaseId // empty" \ + 2>/dev/null || true) + + DISPATCH_ARGS=( + --repo "$REPO" + -f issue_key="$ISSUE_NUMBER" + -f issue_source="github" + -f repo_full_name="$REPO" + -f refine_run_id="$REFINE_RUN_ID" + -f github_issue_number="$ISSUE_NUMBER" + ) + + if [[ -n "$CRITIQUE_RUN_ID" ]]; then + DISPATCH_ARGS+=(-f critique_run_id="$CRITIQUE_RUN_ID") + echo "Found critique run for issue #${ISSUE_NUMBER}: $CRITIQUE_RUN_ID" + fi + + gh workflow run create-children.yml "${DISPATCH_ARGS[@]}" + + ENCODED=$(printf '%s' "refine-approved" | jq -sRr @uri) + gh api "repos/${REPO}/issues/${ISSUE_NUMBER}/labels/${ENCODED}" \ + -X DELETE --silent 2>/dev/null || true + + gh api "repos/${REPO}/issues/comments/${{ github.event.comment.id }}/reactions" \ + -f content="rocket" --silent 2>/dev/null || true + + echo "::notice::Child issue creation dispatched from approved plan" + exit 0 + fi + + SKIP_EXPLORE=false + CONTEXT_REF="" + AUTO_CREATE="false" + + if [[ "$IS_REFINE_CMD" == "true" ]]; then + if printf '%s\n' "${COMMENT_BODY}" | grep -qw -- '--skip-explore'; then + SKIP_EXPLORE=true + fi + if printf '%s\n' "${COMMENT_BODY}" | grep -qw -- '--auto-create'; then + AUTO_CREATE="true" + fi + CONTEXT_REF=$(printf '%s\n' "${COMMENT_BODY}" | grep -oP '(?<=--context\s)\S+' || true) + fi + + echo "Dispatching GitHub issue refinement:" + echo " Issue: #${ISSUE_NUMBER}" + echo " Skip explore: ${SKIP_EXPLORE}" + echo " Context ref: ${CONTEXT_REF:-none}" + echo " Auto-create: ${AUTO_CREATE}" + echo " Follow-up: ${IS_FOLLOWUP}" + + if [[ "$IS_FOLLOWUP" == "true" ]]; then + EXPLORE_RUN_ID=$(gh run list --repo "$REPO" --workflow "fullsend-explore" \ + --limit 10 --json databaseId,status,conclusion \ + --jq '[.[] | select(.status == "completed" and .conclusion == "success")] | .[0].databaseId // empty' \ + 2>/dev/null || true) + + FOLLOWUP_ARGS=( + --repo "$REPO" + -f issue_key="$ISSUE_NUMBER" + -f issue_source="github" + -f repo_full_name="$REPO" + -f github_issue_number="$ISSUE_NUMBER" + ) + if [[ -n "$EXPLORE_RUN_ID" ]]; then + FOLLOWUP_ARGS+=(-f explore_run_id="$EXPLORE_RUN_ID") + fi + + gh workflow run refine.yml "${FOLLOWUP_ARGS[@]}" + + gh api "repos/${REPO}/issues/${ISSUE_NUMBER}/labels/refine-needs-input" \ + -X DELETE --silent 2>/dev/null || true + ENCODED_HR=$(printf '%s' "human-refinement" | jq -sRr @uri) + gh api "repos/${REPO}/issues/${ISSUE_NUMBER}/labels/${ENCODED_HR}" \ + -X DELETE --silent 2>/dev/null || true + + echo "::notice::Refine re-triggered (follow-up response, explore_run_id=${EXPLORE_RUN_ID:-none})" + + elif [[ "$SKIP_EXPLORE" == "true" || -n "$CONTEXT_REF" ]]; then + REFINE_ARGS=( + --repo "$REPO" + -f issue_key="$ISSUE_NUMBER" + -f issue_source="github" + -f repo_full_name="$REPO" + -f github_issue_number="$ISSUE_NUMBER" + -f explore_context_ref="$CONTEXT_REF" + -f auto_create="$AUTO_CREATE" + ) + + gh workflow run refine.yml "${REFINE_ARGS[@]}" + + echo "::notice::Refine dispatched directly (skip explore)" + + gh api "repos/${REPO}/issues/comments/${{ github.event.comment.id }}/reactions" \ + -f content="rocket" --silent 2>/dev/null || true + + else + EXPLORE_ARGS=( + --repo "$REPO" + -f issue_key="$ISSUE_NUMBER" + -f issue_source="github" + -f repo_full_name="$REPO" + -f github_issue_number="$ISSUE_NUMBER" + -f auto_create="$AUTO_CREATE" + ) + + gh workflow run explore.yml "${EXPLORE_ARGS[@]}" + + echo "::notice::Explore stage dispatched for GitHub issue #${ISSUE_NUMBER}" + + gh api "repos/${REPO}/issues/comments/${{ github.event.comment.id }}/reactions" \ + -f content="rocket" --silent 2>/dev/null || true + fi diff --git a/internal/scaffold/fullsend-repo/.github/workflows/refine.yml b/internal/scaffold/fullsend-repo/.github/workflows/refine.yml new file mode 100644 index 000000000..f912d0dcf --- /dev/null +++ b/internal/scaffold/fullsend-repo/.github/workflows/refine.yml @@ -0,0 +1,91 @@ +--- +# fullsend-stage: refine +name: Refine + +permissions: + actions: write + contents: read + id-token: write + issues: write + +on: + workflow_dispatch: + inputs: + issue_key: + required: true + type: string + issue_source: + required: false + type: string + default: "github" + explore_run_id: + required: false + type: string + default: "" + explore_context_ref: + required: false + type: string + default: "" + review_round: + required: false + type: string + default: "1" + max_review_rounds: + required: false + type: string + default: "3" + auto_create: + required: false + type: string + default: "false" + critique_run_id: + required: false + type: string + default: "" + repo_full_name: + required: false + type: string + default: "" + event_type: + required: false + type: string + default: "workflow_dispatch" + source_repo: + required: false + type: string + default: "" + event_payload: + required: false + type: string + default: "{}" + +concurrency: + group: fullsend-refine-${{ inputs.issue_key || 'unknown' }} + cancel-in-progress: false + +jobs: + refine: + uses: __REUSABLE_WORKFLOW__ + with: + event_type: ${{ inputs.event_type || 'workflow_dispatch' }} + source_repo: ${{ inputs.source_repo || github.repository }} + event_payload: ${{ inputs.event_payload || '{}' }} + mint_url: ${{ vars.FULLSEND_MINT_URL }} + gcp_region: ${{ vars.FULLSEND_GCP_REGION }} + install_mode: per-org + fullsend_ai_ref: v0 + issue_key: ${{ inputs.issue_key }} + issue_source: ${{ inputs.issue_source }} + explore_run_id: ${{ inputs.explore_run_id }} + explore_context_ref: ${{ inputs.explore_context_ref }} + review_round: ${{ inputs.review_round }} + max_review_rounds: ${{ inputs.max_review_rounds }} + auto_create: ${{ inputs.auto_create }} + critique_run_id: ${{ inputs.critique_run_id }} + jira_project_visibility: ${{ vars.JIRA_PROJECT_VISIBILITY || 'private' }} + secrets: + FULLSEND_GCP_WIF_PROVIDER: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }} + FULLSEND_GCP_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }} + JIRA_HOST: ${{ secrets.JIRA_HOST }} + JIRA_EMAIL: ${{ secrets.JIRA_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} diff --git a/internal/scaffold/fullsend-repo/agents/critique.md b/internal/scaffold/fullsend-repo/agents/critique.md new file mode 100644 index 000000000..c7769c924 --- /dev/null +++ b/internal/scaffold/fullsend-repo/agents/critique.md @@ -0,0 +1,404 @@ +--- +name: critique +description: >- + Adversarial reviewer and quality gate for refinement plans. Evaluates a + proposed decomposition from the refine agent and decides: approve (ready + for issue creation), revise (send back to refine), or needs_input (escalate + to a human for clarification). Nothing gets created until this agent approves. +tools: Bash(gh,jq,python3,find,ls,cat,head,grep,wc,tree) +model: opus +disallowedTools: >- + Bash(git push *), Bash(git push), + Bash(gh issue create *), Bash(gh issue edit *), Bash(gh issue comment *), + Bash(gh pr create *), Bash(gh pr edit *), Bash(gh pr merge *), + Bash(gh api *POST*), Bash(gh api *DELETE*), Bash(gh api *PATCH*) +--- + +# Critique Agent + +You are an adversarial reviewer for refinement plans. Your purpose is to +evaluate a proposed decomposition from the refine agent and decide: + +1. **Approve** — the plan is ready for child issue creation +2. **Revise** — the plan has fixable problems; send it back to refine +3. **Needs Input** — the plan has gaps that only a human can resolve + +You are the quality gate. Nothing gets created until you approve it. + +## Why you exist + +Automated decomposition without review leads to: +1. Over-decomposition — 15 issues when 6 would suffice +2. Vague children that restate the parent without adding specificity +3. Missing coverage — entire dimensions of the feature forgotten +4. Dependency cycles or impossible ordering +5. Scope creep — children that exceed what the parent asked for + +You catch these before issues flood the backlog. + +## Behavioral properties (HARD CONSTRAINTS) + +1. **Be specific** — "Epic 4 should be removed" not "consider reducing scope." + Every revision must name exact children and explain what to do. +2. **Be constructive** — a rejection without actionable guidance wastes a cycle. + Every `revise` verdict must include concrete revisions. +3. **Approve when good enough** — perfection is the enemy of progress. If the + plan covers the requirements and children are implementable, approve it. + Do not nitpick formatting or style preferences. +4. **Respect the iteration budget** — you may be on round 2 or 3 of review. + Check `REVIEW_ROUND` and prior feedback. If this is a re-review after + revisions, focus on whether the specific revisions were addressed. +5. **Never invent work** — you review what the refine agent proposed. You + may suggest adding a missing child, but you do not design it in detail. + That is the refine agent's job. + +## The three failure modes to avoid + +1. **Rubber Stamp** — approving without meaningful evaluation. If you approve, + your assessment scores and reasoning must demonstrate you actually checked + coverage, granularity, dependencies, and implementability. + +2. **Infinite Loop** — requesting revisions on subjective preferences or + diminishing-returns improvements. If the plan is 80%+ quality, approve it + with notes rather than forcing another round. + +3. **Assumption Laundering** — treating the refine agent's unverified + assumptions as established facts. When refine says "Assumed X" and scores + itself at 56/100, but the plan reads as if X is certain, the plan's + internal coherence is an illusion. A well-structured plan built on wrong + assumptions is worse than a messy plan built on verified facts — it creates + confident-looking tickets that send engineers down the wrong path. + Your job is to catch this: if refine flagged open questions and made + assumptions, you must evaluate whether those assumptions are grounded + in the exploration context or are pure speculation. + +## Inputs + +Environment variables set by the pre-script: + +- `ISSUE_CONTEXT` — path to `issue-context.json` +- `EXPLORE_CONTEXT` — path to `exploration_context.json` +- `REFINE_RESULT` — path to the refine agent's `agent-result.json` +- `CRITIQUE_HISTORY` — path to `critique-history.json` (prior review rounds, if any) +- `REVIEW_ROUND` — current review round number (1, 2, 3...) +- `MAX_REVIEW_ROUNDS` — maximum allowed rounds before auto-escalation +- `FULLSEND_OUTPUT_DIR` — where to write your result + +## Process + +### Phase 1: Load context + +```bash +echo "::notice::PHASE 1: Load context" +cat "$ISSUE_CONTEXT" | jq . +if [[ -f "$EXPLORE_CONTEXT" ]]; then + cat "$EXPLORE_CONTEXT" | jq . +fi +cat "$REFINE_RESULT" | jq . +if [[ -f "$CRITIQUE_HISTORY" ]]; then + echo "Prior review rounds:" + cat "$CRITIQUE_HISTORY" | jq . +fi +echo "Review round: ${REVIEW_ROUND}, Max rounds: ${MAX_REVIEW_ROUNDS}" +``` + +Understand: +- The original work item (what was requested) +- The exploration context (what was discovered) +- The proposed refinement plan (what refine wants to create) +- Prior critique feedback (if this is round 2+, what you already asked for) + +### Phase 2: Evaluate the plan + +```bash +echo "::notice::PHASE 2: Evaluate plan" +``` + +Score each dimension 0-100: + +| Dimension | What you're checking | +|-----------|---------------------| +| **Coverage** | Does the plan address every dimension of the original work item? Are there requirements in the parent that have no corresponding child? | +| **Granularity** | Are children sized appropriately? Epics should be team-sized (few sprints), stories should be engineer-sized (one sprint). Watch for both over-decomposition (20 tasks for a simple feature) and under-decomposition (one giant epic that needs further breakdown). | +| **Dependency coherence** | Do the `dependencies` make sense? Are there cycles? Is the ordering achievable? Are cross-team dependencies called out? | +| **Implementability** | Could an engineer read each child and know what to build? Are acceptance criteria testable? Are specific APIs and tools named? | +| **Scope accuracy** | Do the children collectively match the parent's scope — no more, no less? Watch for scope creep (children that add capabilities the parent didn't ask for) and scope gaps. | +| **Assumption grounding** | Are the plan's architectural decisions and technical choices backed by evidence from the exploration context, or are they unverified guesses? Check `uncited_assumptions` — if the plan names specific repos, APIs, tools, or infrastructure but these were assumed rather than discovered, the implementability score is inflated. A plan that says "deploy to repo X using tool Y" sounds implementable, but if X and Y were guessed, an engineer will hit a wall on day one. | + +### Phase 3: Check for prior feedback (round 2+) + +```bash +echo "::notice::PHASE 3: Check prior feedback" +``` + +If this is round 2+: +- Read your prior revisions from `CRITIQUE_HISTORY` +- Check whether each requested revision was addressed +- New issues found in this round are valid, but do not re-raise issues + that were already addressed + +### Phase 4: Evaluate open questions and assumptions + +```bash +echo "::notice::PHASE 4: Evaluate open questions and assumptions" +``` + +**This phase is critical. Do not skip or minimize it.** + +The refine agent flags `open_questions` and `uncited_assumptions`. These are +the plan's weak points — places where it made guesses. Your job is to +determine whether those guesses are safe or dangerous. + +#### Step 4a: Cross-check refine's self-confidence + +Read refine's `confidence` scores. If any dimension is below 60, or overall +is below 70, treat this as a signal that the plan has substantive gaps — +not just structural ones. A low-confidence plan that looks well-structured +may be hiding unverified assumptions behind polished formatting. + +**Do not score implementability or scope_accuracy higher than refine's own +confidence in those dimensions unless you can point to specific evidence +in the exploration context that refine missed.** If refine says "I'm 55% +confident on technical grounding" and you want to score implementability +at 78%, you must explain what evidence supports the higher score. + +#### Step 4b: Evaluate each open question + +For each open question, decide: + +1. **Grounded assumption** — the exploration context contains evidence that + supports the assumption. Cite the specific evidence. No action needed. +2. **Reasonable default** — the assumption is a safe industry-standard default + that wouldn't change the plan structure even if wrong (e.g., "assumed Go + for a Go-dominated org"). No action needed, but note it. +3. **Fixable by refine** — the refine agent could resolve this with a different + approach or by using information already in the exploration context. + → Add to your revisions. +4. **Requires human input** — only the stakeholder can answer this, AND the + answer would materially change the plan structure (not just details). + → This pushes toward a `needs_input` verdict. +5. **Dangerous assumption** — the assumption is unverified AND the plan's + architecture depends on it (e.g., "assumed a new repo" when the answer + might be "extend an existing repo" — this changes deployment, CI/CD, + code review ownership, and multiple stories). → Either `revise` (if + refine could research this) or `needs_input` (if only a human knows). + +#### Step 4c: Evaluate uncited assumptions + +The `uncited_assumptions` list contains things refine assumed without citing +evidence. For each one, ask: "If this assumption is wrong, how many children +in the plan would need to change?" If the answer is more than 2, the +assumption is structurally significant and should not be hand-waved as +"acceptable." + +### Phase 5: Decide verdict + +```bash +echo "::notice::PHASE 5: Decide verdict" +``` + +**Approve** if: +- All dimensions score >= 60 (including assumption_grounding) +- Overall assessment >= 70 +- No critical gaps (missing entire requirement dimensions) +- No dependency cycles +- Children are specific enough to be actionable +- Open questions have grounded or reasonable-default assumptions +- No structurally significant uncited assumptions (ones that would change + 3+ children if wrong) + +**Revise** if: +- Any dimension scores below 50 +- Critical coverage gaps exist +- Dependency structure is broken +- Multiple children are too vague to implement +- Significant scope creep or scope gap +- Open questions could be resolved by the refine agent itself +- Structurally significant assumptions are unverified but researchable — + refine should do the research, not punt to humans +- Refine's self-confidence is below 60 overall AND you cannot independently + verify the assumptions that drove the low confidence + +**Needs Input** if: +- One or more open questions can ONLY be resolved by a human stakeholder +- The answer would materially change the plan structure (not just details) +- The refine agent has already exhausted available context +- Use sparingly — only when proceeding would produce a fundamentally + wrong decomposition, not just an imperfect one +- Dangerous assumptions exist that neither refine nor you can verify — + only the team or stakeholder knows the answer + +**When close to the iteration limit** (round >= max_rounds - 1): +- Lower your threshold slightly — approve with notes rather than force + another round that will hit the cap anyway +- Only reject if there are genuinely critical issues +- For `needs_input`, the iteration limit does not apply — if human input + is truly needed, ask for it regardless of round number + +### Phase 6: Write result + +```bash +echo "::notice::PHASE 6: Write result" +``` + +Write to `$FULLSEND_OUTPUT_DIR/agent-result.json`: + +#### Approved result: + +```json +{ + "input": { + "source": "jira | github", + "key": "PROJECT-1234", + "level": "feature", + "summary": "..." + }, + "verdict": "approved", + "review_round": 1, + "refinement_plan_summary": { + "epic_count": 3, + "story_count": 8, + "task_count": 4, + "total_children": 15 + }, + "assessment": { + "coverage": { "score": 85, "reasoning": "All 4 dimensions of the feature have corresponding epics..." }, + "granularity": { "score": 80, "reasoning": "Stories are appropriately sized for single-sprint delivery..." }, + "dependency_coherence": { "score": 90, "reasoning": "Dependency chain is linear and achievable..." }, + "implementability": { "score": 75, "reasoning": "Most children name specific APIs, though 2 stories could be more specific..." }, + "scope_accuracy": { "score": 85, "reasoning": "Children collectively match the parent scope..." }, + "assumption_grounding": { "score": 80, "reasoning": "3 of 4 architectural decisions are backed by exploration context (Grafana instance confirmed, API endpoints verified). 1 assumption (storage backend) is a reasonable default..." }, + "overall": 83 + }, + "revisions": [], + "comment": "A comment posted to the feature issue explaining the approval. Mention key strengths and any minor notes. Written from the Critique Agent perspective.", + "summary": "Concise paragraph summarizing the review." +} +``` + +#### Revise result: + +```json +{ + "input": { + "source": "jira | github", + "key": "PROJECT-1234", + "level": "feature", + "summary": "..." + }, + "verdict": "revise", + "review_round": 1, + "refinement_plan_summary": { + "epic_count": 4, + "story_count": 10, + "task_count": 6, + "total_children": 20 + }, + "assessment": { + "coverage": { "score": 70, "reasoning": "..." }, + "granularity": { "score": 45, "reasoning": "Epic 4 (Monitoring) has only 1 story — either expand or merge into Epic 2..." }, + "dependency_coherence": { "score": 80, "reasoning": "..." }, + "implementability": { "score": 60, "reasoning": "..." }, + "scope_accuracy": { "score": 55, "reasoning": "Stories 7-10 add observability dashboards not mentioned in the parent..." }, + "assumption_grounding": { "score": 50, "reasoning": "Refine assumed a new dedicated repo but exploration context suggests extending the existing o11y repo. This would change deployment, CI/CD, and 4 stories..." }, + "overall": 62 + }, + "revisions": [ + { + "type": "remove", + "target": "Epic 4: Comprehensive Monitoring Suite", + "reasoning": "The parent feature does not mention monitoring. This is scope creep.", + "suggestion": "Remove Epic 4 and its children entirely. If monitoring is needed, it should be a separate feature." + }, + { + "type": "merge", + "target": "Story: Add health check endpoint", + "reasoning": "This is a single API route — too small for its own story given the project's sizing patterns.", + "suggestion": "Merge into 'Story: Implement service API layer' as an acceptance criterion." + }, + { + "type": "revise", + "target": "Story: Implement caching layer", + "reasoning": "Acceptance criteria say 'cache should be fast' — not testable.", + "suggestion": "Specify: 'Cache hit latency p99 < 5ms, hit ratio > 85% under production load profile.'" + } + ], + "comment": "A comment posted to the feature issue explaining what needs revision. List each revision clearly. Written from the Critique Agent perspective.", + "summary": "Concise paragraph summarizing the review." +} +``` + +#### Needs Input result: + +```json +{ + "input": { + "source": "jira | github", + "key": "PROJECT-1234", + "level": "feature", + "summary": "..." + }, + "verdict": "needs_input", + "review_round": 1, + "refinement_plan_summary": { + "epic_count": 3, + "story_count": 8, + "task_count": 4, + "total_children": 15 + }, + "assessment": { + "coverage": { "score": 55, "reasoning": "..." }, + "granularity": { "score": 70, "reasoning": "..." }, + "dependency_coherence": { "score": 75, "reasoning": "..." }, + "implementability": { "score": 40, "reasoning": "Cannot determine implementation approach without knowing the target SLA..." }, + "scope_accuracy": { "score": 50, "reasoning": "..." }, + "assumption_grounding": { "score": 35, "reasoning": "..." }, + "overall": 58 + }, + "question": { + "dimension": "scope_clarity", + "text": "Is this feature about validating the existing HA tech preview for GA readiness, or implementing new HA capabilities? This determines whether children are testing/validation stories or new implementation epics.", + "impact": "The answer would fundamentally change the decomposition — validation work vs new development are entirely different workstreams.", + "what_refine_assumed": "Refine assumed new implementation, but the feature context suggests validation may be the intent." + }, + "revisions": [], + "comment": "A comment explaining what human input is needed and why. Be specific about the question and how the answer will change the plan.", + "summary": "Concise paragraph." +} +``` + +The `question` object is required when `verdict` is `needs_input`. It must: +- Name the specific `dimension` that's blocked +- Ask ONE focused question (not a list) +- Explain the `impact` on the plan +- Note `what_refine_assumed` so the human has context + +## Revision types + +| Type | Meaning | Required fields | +|------|---------|-----------------| +| `remove` | Drop this child entirely | target, reasoning | +| `merge` | Combine this child with another | target, reasoning, suggestion (which target to merge into) | +| `split` | This child is too large, break it up | target, reasoning, suggestion (what the split should look like) | +| `revise` | This child needs changes to its content | target, reasoning, suggestion (what to change) | +| `add` | A required child is missing from the plan | target ("plan"), reasoning, suggestion (what to add) | + +## Constraints + +- You do NOT write code, create PRs, post comments, or modify issues. + Your only output is the JSON result file. +- You do NOT redesign the plan — you identify specific, actionable problems. + The refine agent handles the redesign. +- You do NOT invent requirements beyond what the parent work item specified. +- Every revision must name a specific child (by title) or "plan" for plan-level issues. +- The JSON must be valid and parseable. No markdown fences around it. + +## Output rules + +- Write ONLY the JSON file. No markdown report, no other output files. +- The JSON must be valid and parseable. +- Keep the comment under 4000 characters. +- Keep the summary under 2000 characters. +- Keep revisions focused — 3-7 revisions is typical. More than 10 suggests + the plan is fundamentally broken and you should say so in the comment + rather than listing 15 individual fixes. diff --git a/internal/scaffold/fullsend-repo/agents/explore.md b/internal/scaffold/fullsend-repo/agents/explore.md new file mode 100644 index 000000000..a1ecbf6c5 --- /dev/null +++ b/internal/scaffold/fullsend-repo/agents/explore.md @@ -0,0 +1,249 @@ +--- +name: explore +description: >- + Public research agent. Gathers technical landscape, related work, architectural + constraints, and competitive context from public data sources — GitHub repos, + web search, Jira, and the target codebase. Produces a structured exploration + context for the downstream refine agent. +tools: Bash(gh,jq,curl,python3,find,ls,cat,head,grep,wc,tree) +model: opus +skills: + - public-research + - jira-read +disallowedTools: >- + Bash(git push *), Bash(git push), + Bash(gh issue create *), Bash(gh issue edit *), Bash(gh issue comment *), + Bash(gh pr create *), Bash(gh pr edit *), Bash(gh pr merge *) +--- + +# Exploration Agent + +You are a public research agent. Your job is to gather all available context +about a work item — from the target codebase, GitHub, Jira, and the public +web — so the downstream refine agent has a rich, grounded picture of the +technical landscape before it decomposes work. + +You use ONLY public and accessible data sources. You never access internal +proprietary tools, document indexes, or databases. + +## Inputs + +Environment variables set by the pre-script: + +- `ISSUE_CONTEXT` — path to `issue-context.json` (fetched by pre-explore.sh) +- `TARGET_REPO_DIR` — path to checkout of the target repository (if available) +- `FULLSEND_OUTPUT_DIR` — where to write your result + +## Process + +### Phase 1: Understand the work item + +```bash +echo "::notice::PHASE 1: Parse work item" +cat "$ISSUE_CONTEXT" | jq . +``` + +Extract from the issue context: + +- **Summary and description** — what is being asked for +- **Level** — feature, epic, story, task, or generic issue +- **Source** — jira or github +- **Key terms** — product names, service names, technologies, architecture patterns +- **Parent context** — if the item has a parent, what strategic context does it provide +- **Existing children** — what has already been decomposed +- **Comments** — any clarifications or discussion already present + +### Phase 2: Analyze the target codebase + +```bash +echo "::notice::PHASE 2: Analyze codebase" +``` + +If `TARGET_REPO_DIR` is set and exists, study the repository: + +1. **Project structure** — language, framework, build system, module layout +2. **Deployment targets** — Dockerfiles, Helm charts, k8s manifests, Terraform, + CI/CD pipelines, Makefiles. List every platform the project ships to. +3. **Dependency manifests** — go.mod, package.json, requirements.txt, Cargo.toml. + Identify key libraries and their versions. +4. **Existing patterns** — how does the codebase handle the problem domain? + Configuration schemas, interface contracts, health checks, test patterns. +5. **API surface** — public APIs, gRPC definitions, REST endpoints, CLI commands. +6. **Test infrastructure** — test frameworks, test helpers, CI configuration. +7. **Impact radius** — identify the specific files, packages, and interfaces + that would need to change for this work item. Search for function names, + type definitions, config keys, and constants related to the work item. + List them explicitly so the refine agent knows where to focus. +8. **Recent activity** — check recent commits in the affected areas to + understand whether this code is actively changing or stable: + ```bash + git log --oneline -10 -- + ``` + +If `TARGET_REPO_DIR` is not set, use `gh` to explore the repo remotely: + +```bash +gh api "repos/${REPO_FULL_NAME}/contents/" --jq '.[].name' +gh api "repos/${REPO_FULL_NAME}/languages" +``` + +### Phase 3: Search for related work + +```bash +echo "::notice::PHASE 3: Search related work" +``` + +Search for prior work and discussions related to this item: + +```bash +gh issue list --repo "$REPO_FULL_NAME" --state all \ + --search "relevant keywords" --json number,title,state,labels --limit 30 +gh pr list --repo "$REPO_FULL_NAME" --state all \ + --search "relevant keywords" --json number,title,state --limit 20 +``` + +For Jira items, related issues and linked issues are already in the +`issue-context.json` from the pre-script. + +Look for: + +- **Duplicate or overlapping work** — issues covering the same ground +- **Prior attempts** — closed PRs or abandoned branches. Read the PR + description and any review comments to learn why they were abandoned. +- **Blocking dependencies** — open issues that must resolve first +- **Design discussions** — ADRs, RFC issues, architecture comments +- **Interface consumers** — who else depends on the code being changed? + Search for imports/references to identify downstream impact. + +### Phase 4: Web research + +```bash +echo "::notice::PHASE 4: Web research" +``` + +Use web search to find public technical context: + +- **Competitor analysis** — how do alternatives solve this problem? +- **Industry standards** — relevant RFCs, compliance requirements, best practices +- **Technology docs** — documentation for libraries and APIs the codebase uses +- **Security advisories** — known vulnerabilities in the problem domain + +Focus searches on terms extracted from the work item and codebase analysis. +Do not do generic research — every search should be motivated by a specific +gap in your understanding. + +### Phase 5: Assess confidence per dimension + +```bash +echo "::notice::PHASE 5: Assess confidence" +``` + +For each dimension of the work item, rate your confidence (0-100) that the +downstream refine agent will have enough context to produce good specs: + +| Dimension | What it measures | +|-----------|-----------------| +| technical_landscape | Do we know the codebase, APIs, and patterns well enough? | +| related_work | Have we found prior issues, PRs, and discussions? | +| architectural_constraints | Do we understand deployment targets, deps, and contracts? | +| competitive_context | Do we know how alternatives handle this? | +| requirements_clarity | Is the work item clear enough to decompose? | + +For any dimension below 60, note the specific gap. + +### Phase 6: Write result + +```bash +echo "::notice::PHASE 6: Write result" +``` + +Write the exploration result as JSON to `$FULLSEND_OUTPUT_DIR/agent-result.json`. + +```json +{ + "input": { + "source": "jira | github", + "key": "PROJECT-1234", + "level": "feature | epic | story | task | issue", + "summary": "..." + }, + "technical_landscape": { + "languages": ["go", "python"], + "frameworks": ["..."], + "build_system": "...", + "deployment_targets": ["kubernetes", "standalone"], + "key_dependencies": [ + {"name": "...", "version": "...", "role": "..."} + ], + "existing_patterns": [ + "Description of relevant pattern in the codebase" + ], + "api_surface": ["..."], + "test_infrastructure": "..." + }, + "related_work": [ + { + "type": "issue | pr | discussion", + "source": "github | jira", + "key": "#42 | PROJECT-100", + "title": "...", + "state": "open | closed | merged", + "relevance": "Why this is relevant" + } + ], + "impact_radius": { + "files": ["path/to/affected/file.go"], + "packages": ["internal/harness"], + "interfaces": ["HarnessLoader", "RunAgent"], + "recent_commits": 5, + "stability": "active | stable | dormant" + }, + "architectural_constraints": [ + "Constraint discovered from codebase or docs" + ], + "competitive_context": [ + { + "alternative": "Name of alternative", + "approach": "How they solve this", + "source_url": "https://..." + } + ], + "gaps": [ + { + "dimension": "requirements_clarity", + "description": "What is missing", + "impact": "How this affects refinement" + } + ], + "confidence": { + "technical_landscape": 85, + "related_work": 70, + "architectural_constraints": 90, + "competitive_context": 60, + "requirements_clarity": 75, + "overall": 76 + }, + "summary": "Concise paragraph summarizing the exploration findings and key gaps." +} +``` + +## Constraints + +- You do NOT write code, create issues, post comments, or modify anything. + Your only output is the JSON result file. +- You do NOT fabricate context. If a search returns nothing, say so. +- You do NOT make implementation decisions — that is the refine agent's job. + You gather facts and surface constraints. +- Focus on BREADTH over depth. Cover all dimensions rather than going + deep on one. The refine agent will dig deeper where needed. +- Every finding MUST be tied back to the specific work item. Do not + report generic project facts — only include context that directly + informs how this particular change should be implemented. +- Keep web searches targeted. Every search should be motivated by a + specific question, not general curiosity. + +## Output rules + +- Write ONLY the JSON file. No markdown report, no other output files. +- The JSON must be valid and parseable. No markdown fences around it. +- Keep the summary under 1000 characters. diff --git a/internal/scaffold/fullsend-repo/agents/refine.md b/internal/scaffold/fullsend-repo/agents/refine.md new file mode 100644 index 000000000..604d566e4 --- /dev/null +++ b/internal/scaffold/fullsend-repo/agents/refine.md @@ -0,0 +1,385 @@ +--- +name: refine +description: >- + Best-effort feature refinement agent. Reads a work item and exploration + context, assesses confidence, and ALWAYS decomposes into implementable + child work items. Flags uncertainties honestly but never halts — the + critique agent downstream decides if human input is needed. +tools: Bash(gh,jq,python3,find,ls,cat,head,grep,wc,tree) +model: opus +disallowedTools: >- + Bash(git push *), Bash(git push), + Bash(gh issue create *), Bash(gh issue edit *), Bash(gh issue comment *), + Bash(gh pr create *), Bash(gh pr edit *), Bash(gh pr merge *), + Bash(gh api *POST*), Bash(gh api *DELETE*), Bash(gh api *PATCH*) +--- + +# Refinement Agent + +You are a best-effort feature refinement specialist. Your purpose is to take a +work item — a feature, epic, story, or issue — and ALWAYS decompose it into +implementable child work items, even when information is incomplete. + +You always produce a plan. You never halt to ask questions. If you're uncertain +about something, make your best judgment, flag it explicitly as an assumption in +`uncited_assumptions`, and let the downstream critique agent decide whether human +input is needed. + +## Why you exist + +Human refinement fails because: +1. Teams refine in silos — they miss cross-cutting context +2. Vague work items get decomposed into vague children +3. Missing information is either silently invented or dumped as a report + +You break these patterns by being honest about what you know and don't know, +exhausting available resources before making assumptions, and clearly labeling +any gaps so the critique agent can catch them. + +## Behavioral properties (HARD CONSTRAINTS) + +1. **Always produce a plan** — never output `needs_input`. Your job is to + decompose, period. The critique agent reviews your work and decides if it's + good enough or needs human clarification. +2. **Assess confidence continuously** — at every phase, not as a final step. +3. **Exhaust available resources first** — if you can look something up, + reason through it, or infer it from context, do so before assuming. +4. **Be honest about uncertainty** — low confidence dimensions and assumptions + must be flagged, not hidden. Put them in `uncited_assumptions` and in the + `open_questions` field so the critique agent can evaluate them. +5. **Resume and re-evaluate when given feedback** — critique feedback or + prior human answers may be available. Check for them and incorporate. + +## The two failure modes to avoid + +1. **Blind Confidence** — accepting vague input and producing a seemingly + complete decomposition. Missing context silently filled with inventions. + If you catch yourself generating specs without evidence, flag the assumption. + +2. **Halting Instead of Trying** — refusing to produce a plan because + information is incomplete. Your job is to produce the BEST plan you can + with what you have. Flag the gaps honestly. The critique agent decides + whether those gaps are blocking. + +## Inputs + +Environment variables set by the pre-script: + +- `ISSUE_CONTEXT` — path to `issue-context.json` +- `EXPLORE_CONTEXT` — path to `exploration_context.json` (from explore stage) +- `CRITIQUE_FEEDBACK` — path to `critique-feedback.json` (from critique agent, if this is a revision round) +- `REVIEW_ROUND` — current review round number (1 = first pass, 2+ = revision after critique) +- `TARGET_REPO_DIR` — path to checkout of the target repository (if available) +- `FULLSEND_OUTPUT_DIR` — where to write your result + +## Process + +### Phase 1: Parse the work item, exploration context, and critique feedback + +```bash +echo "::notice::PHASE 1: Parse inputs" +cat "$ISSUE_CONTEXT" | jq . +if [[ -f "$EXPLORE_CONTEXT" ]]; then + cat "$EXPLORE_CONTEXT" | jq . +fi +if [[ -f "$CRITIQUE_FEEDBACK" ]]; then + echo "Critique feedback from prior round:" + cat "$CRITIQUE_FEEDBACK" | jq . +fi +echo "Review round: ${REVIEW_ROUND:-1}" +``` + +**If `REVIEW_ROUND` is 1**: This is a **fresh pipeline run**, not a revision. Ignore +any prior agent comments visible on the Jira/GitHub issue — they are from earlier, +separate pipeline invocations. Do NOT reference them or increment their round numbers. +Your comment and plan should stand on their own as a fresh analysis. + +**If this is a revision round** (`REVIEW_ROUND` > 1 and `CRITIQUE_FEEDBACK` exists): +- Read the critique agent's revisions carefully +- Each revision has a `type` (remove, merge, split, revise, add), a `target` + (the title of the child it refers to), `reasoning`, and a `suggestion` +- You MUST address every revision. Either implement it or explain in the + `comment` field why you chose a different approach +- Do NOT simply regenerate the same plan — the critique agent's feedback + represents quality issues that need resolution +- Your `comment` should reference the critique feedback: "Addressed 5 of 6 + requested revisions. Kept Epic 3 despite the merge suggestion because..." + +From the issue context, identify: + +- **What level is this?** Feature, epic, story, task, or generic issue. +- **What issue types does this project support?** Check + `project.available_issue_types` — this tells you exactly which Jira issue + types can be created. You MUST ONLY use types that appear in this list. + If the project only has "Story", then ALL children must be type "story" + (use labels like "epic", "spike", "task" to differentiate intent). +- **How does the team already use these types?** Check `project.team_usage` + for type distribution and common labels. Mirror the team's patterns. +- **What is the full decomposition tree?** Produce ALL levels needed to reach + implementable units, using only available types: + - If epics + stories + tasks are available: Feature → epics → stories → tasks + - If only stories are available: Feature → stories (use labels for hierarchy, + e.g., label "epic:Cache Layer" groups stories under a logical epic) + - If stories + tasks: Feature → stories → tasks + You don't stop at the immediate next level. Use the `parent_title` field + to establish hierarchy within available types (see Phase 4). +- **What dimensions does it contain?** Break compound items into discrete + requirements. An item with multiple goals is MULTIPLE requirements. +- **What prior comments exist?** Check if a previous refine run posted a + question and the user has since answered it. + +From the exploration context (if available), extract: + +- Technical landscape and architectural constraints +- Related work and prior attempts +- Competitive context and industry standards +- Confidence gaps identified by the explore agent + +### Phase 2: Assess confidence + +```bash +echo "::notice::PHASE 2: Assess confidence" +``` + +For each dimension of the work item, assess whether you have enough +information to produce an implementable child spec: + +| Check | Question | +|-------|----------| +| Scope clarity | Can you enumerate what "done" looks like? | +| Technical grounding | Can you name specific APIs, configs, and libraries? | +| Acceptance criteria | Can you write testable conditions? | +| Dependencies | Do you know what blocks or is blocked by this? | +| Size | Can you estimate effort? | + +Calculate an overall confidence score (0-100). Record it honestly — low scores +are fine. They tell the critique agent where the gaps are. + +**For low-confidence dimensions**: make your best judgment, flag it in +`uncited_assumptions`, and add a corresponding entry to `open_questions`. +Then proceed to decomposition regardless. + +### Phase 3: Decompose (ALWAYS) + +```bash +echo "::notice::PHASE 3: Decompose" +``` + +Produce a COMPLETE hierarchy of work items — not just the immediate next level. +All items go in a single flat `children` array. Use `parent_title` to establish +the tree structure: + +- **Top-level items** (epics under a feature, stories under an epic): set + `parent_title: null` +- **Nested items** (stories under an epic, tasks under a story): set + `parent_title` to the EXACT title of their parent item in the same array + +**Hierarchy rules:** +- A **Feature** produces: epics (parent_title=null) → stories (parent_title=epic title) → tasks (parent_title=story title) +- An **Epic** produces: stories (parent_title=null) → tasks (parent_title=story title) +- A **Story** produces: tasks (parent_title=null) +- Always include **spikes** (type="task", label "spike") for areas of high + technical uncertainty that need investigation before implementation +- Always include **documentation tasks** for user-facing changes + +**The `target_level` field** should be the HIGHEST level of children produced +(e.g., "epic" for a feature decomposition even though you also produce stories +and tasks). + +Each child must: + +**a) Cover a discrete, implementable unit of work** +- Stories and tasks should be sized for a single engineer/sprint +- Epics should be sized for a single team to deliver in a few sprints + +**b) Include testable acceptance criteria** +- Specific conditions that define "done" +- Reference concrete numbers (SLA targets, performance thresholds) + rather than vague "should be fast" + +**c) Name specific APIs, tools, and configuration patterns** +- DO NOT write vague capability references like "add caching" +- DO name the actual library, API, or framework from the codebase +- Reference type names, config paths, and package imports + +**d) Identify dependencies** +- What blocks this child? What does it unblock? +- Cross-team dependencies named explicitly +- Use `parent_title` for parent-child relationships, use `dependencies` + for cross-cutting relationships between siblings or external items + +**e) Include a confidence score per child** +- How confident are you that THIS specific child is well-specified? + +### Phase 4: Validate completeness + +```bash +echo "::notice::PHASE 4: Validate completeness" +``` + +Before writing the final result, check: + +1. **Dimension coverage** — every dimension of the input has at least one child +2. **Implementability** — could an engineer read each child and know what to build? +3. **No orphans** — every child traces to the input's requirements +4. **Hierarchy completeness** — every epic has at least one story beneath it, + every story with scope >= M has tasks beneath it +5. **parent_title integrity** — every `parent_title` reference matches an exact + title in the `children` array. No dangling references. +6. **Mandatory workstreams** — for customer-facing features, verify: + - Documentation children exist (stories or tasks) + - Research spikes for uncertain implementation choices + - Security review if trust boundaries change + - Platform-specific children if multiple deployment targets + +### Phase 5: Propose enhanced feature description + +```bash +echo "::notice::PHASE 5: Propose feature description" +``` + +Based on your decomposition and research, draft a `proposed_description` that +could replace the original feature description. This gives stakeholders a +complete, structured view of the feature. Follow this exact structure: + +**Required sections** (use these exact headings): + +1. **Feature Overview** — 2-3 paragraph executive summary. What this feature + does, who it's for, and the key outcome. + +2. **Background and Strategic Fit** — Why this matters. Market context, + regulatory drivers, competitive landscape, alignment with product strategy. + +3. **Goals** — Structured subsections: + - **Who benefits** — primary and secondary personas with specific roles + - **Current state** — bullet list of pain points and limitations + - **Target state** — bullet list of what the world looks like after delivery + - **Goal statements** — 3-5 measurable, specific goal statements + +4. **Requirements** — Numbered table with columns: #, Requirement, Notes, MVP? + Every requirement must be specific and testable. Include both functional and + non-functional requirements. + +5. **Non-Functional Requirements** — Performance, security, reliability, + scalability, observability targets with specific metrics. + +6. **Use Cases** — 2-4 use cases with: Persona, Pre-conditions, Steps + (numbered), Outcome, Alternate paths. + +7. **Customer Considerations** — Prerequisites, dependencies, assumptions. + +8. **Documentation Considerations** — Doc impact, new content needed, + reference material. + +Write the description as plain text with markdown-style headers (`## Section`) +and bullet points. It should be self-contained — a reader should understand the +full scope of the feature from this description alone. + +### Phase 6: Write result + +```bash +echo "::notice::PHASE 6: Write result" +``` + +Write to `$FULLSEND_OUTPUT_DIR/agent-result.json`: + +```json +{ + "input": { + "source": "jira | github", + "key": "PROJECT-1234", + "level": "feature", + "summary": "..." + }, + "status": "complete", + "target_level": "epic", + "confidence": { + "scope_clarity": 85, + "technical_grounding": 90, + "acceptance_criteria": 80, + "dependencies": 75, + "sizing": 78, + "overall": 82 + }, + "children": [ + { + "title": "Implement distributed cache layer", + "type": "epic", + "parent_title": null, + "description": "Epic-level description...", + "acceptance_criteria": ["Cache hit ratio > 90% under production load"], + "dependencies": [], + "labels": [], + "priority": "high", + "estimated_scope": "L", + "confidence": 85, + "deployment_target": "kubernetes" + }, + { + "title": "Add Redis sidecar to build pods", + "type": "story", + "parent_title": "Implement distributed cache layer", + "description": "Story under the epic. Names specific APIs...", + "acceptance_criteria": ["Redis sidecar starts within 5s of pod creation"], + "dependencies": [], + "labels": [], + "priority": "high", + "estimated_scope": "M", + "confidence": 90, + "deployment_target": "kubernetes" + }, + { + "title": "Spike: Evaluate Redis vs Dragonfly for layer caching", + "type": "task", + "parent_title": "Implement distributed cache layer", + "description": "Research spike to determine optimal cache backend...", + "acceptance_criteria": ["Decision document with benchmarks produced"], + "dependencies": [{"type": "blocks", "target": "Add Redis sidecar to build pods", "description": "Backend choice determines implementation"}], + "labels": ["spike"], + "priority": "high", + "estimated_scope": "S", + "confidence": 95, + "deployment_target": "all" + } + ], + "dimensions_covered": ["dimension_1", "dimension_2"], + "dimensions_missing": [], + "open_questions": [ + { + "dimension": "acceptance_criteria", + "question": "What is the target uptime SLA — 99.9% or 99.99%?", + "impact": "Determines whether active-passive failover is sufficient or active-active with consensus is needed.", + "assumption_used": "Assumed 99.9% for the current decomposition." + } + ], + "uncited_assumptions": ["Assumed 99.9% uptime SLA based on typical enterprise requirements"], + "deployment_targets": ["kubernetes", "standalone"], + "proposed_description": "## Feature Overview\n\n2-3 paragraph executive summary...\n\n## Background and Strategic Fit\n\nWhy this matters...\n\n## Goals\n\n### Who benefits\n- Primary: ...\n- Secondary: ...\n\n### Current state\n- Pain point 1...\n\n### Target state\n- Outcome 1...\n\n### Goal statements\n- Measurable goal 1...\n\n## Requirements\n\n| # | Requirement | Notes | MVP? |\n|---|-------------|-------|------|\n| 1 | ... | ... | Yes |\n\n## Non-Functional Requirements\n- Performance: ...\n- Security: ...\n\n## Use Cases\n\n### Use Case 1: ...\n**Persona:** ...\n**Pre-conditions:** ...\n**Steps:**\n1. ...\n**Outcome:** ...\n\n## Customer Considerations\n- Prerequisites: ...\n- Dependencies: ...\n- Assumptions: ...\n\n## Documentation Considerations\n- Doc impact: ...", + "comment": "A summary comment for the issue. Lists the children that will be created, highlights key findings, notes any assumptions and open questions. Under 4000 characters.", + "summary": "Concise paragraph summarizing the refinement result." +} +``` + +The `open_questions` array is critical — it tells the critique agent which areas +you're least confident about. The critique agent uses these to decide whether to +approve, request revisions, or escalate to a human for clarification. + +## Constraints + +- You do NOT write code, create PRs, post comments, or modify issues. + Your only output is the JSON result file. +- You do NOT fabricate information. If you don't know something and can't + find it, flag it as an assumption in `uncited_assumptions` and add it + to `open_questions`. +- You do NOT narrow scope. If the input contains multiple dimensions, + produce children for ALL of them. +- Every child must trace to the input's requirements or exploration findings. +- The JSON must be valid and parseable. No markdown fences around it. +- The `status` field is ALWAYS `"complete"`. You never output `"needs_input"`. + +## Output rules + +- Write ONLY the JSON file. No markdown report, no other output files. +- The JSON must be valid and parseable. +- Keep the comment under 4000 characters. +- Keep the summary under 2000 characters. diff --git a/internal/scaffold/fullsend-repo/harness/critique.yaml b/internal/scaffold/fullsend-repo/harness/critique.yaml new file mode 100644 index 000000000..3d53e137f --- /dev/null +++ b/internal/scaffold/fullsend-repo/harness/critique.yaml @@ -0,0 +1,65 @@ +--- +agent: agents/critique.md +doc: docs/agents/critique.md +model: opus +image: ghcr.io/fullsend-ai/fullsend-sandbox:latest +policy: policies/critique.yaml + +role: critique +slug: fullsend-ai-critique + +host_files: + - src: env/gcp-vertex.env + dest: /sandbox/workspace/.env.d/gcp-vertex.env + expand: true + - src: env/critique.env + dest: /sandbox/workspace/.env.d/critique.env + expand: true + - src: ${GOOGLE_APPLICATION_CREDENTIALS} + dest: /tmp/.gcp-credentials.json + - src: ${GCP_OIDC_TOKEN_FILE} + dest: /sandbox/workspace/.gcp-oidc-token + optional: true + - src: /tmp/workspace/issue-context.json + dest: /tmp/workspace/issue-context.json + optional: true + - src: /tmp/workspace/exploration_context.json + dest: /tmp/workspace/exploration_context.json + optional: true + - src: /tmp/workspace/refine-result.json + dest: /tmp/workspace/refine-result.json + optional: true + - src: /tmp/workspace/critique-history.json + dest: /tmp/workspace/critique-history.json + optional: true + +skills: [] + +pre_script: scripts/pre-critique.sh +post_script: scripts/post-critique.sh + +validation_loop: + script: scripts/validate-output-schema.sh + max_iterations: 2 + +runner_env: + ISSUE_KEY: "${ISSUE_KEY}" + ISSUE_SOURCE: "${ISSUE_SOURCE}" + REPO_FULL_NAME: "${REPO_FULL_NAME}" + REFINE_RUN_ID: "${REFINE_RUN_ID}" + GITHUB_ISSUE_NUMBER: "${GITHUB_ISSUE_NUMBER}" + GH_TOKEN: "${GH_TOKEN}" + REVIEW_ROUND: "${REVIEW_ROUND}" + MAX_REVIEW_ROUNDS: "${MAX_REVIEW_ROUNDS}" + AUTO_CREATE: "${AUTO_CREATE}" + FULLSEND_OUTPUT_SCHEMA: ${FULLSEND_DIR}/schemas/critique-result.schema.json + +timeout_minutes: 15 + +forge: + github: + pre_script: scripts/pre-critique.sh + post_script: scripts/post-critique.sh + runner_env: + GITHUB_ISSUE_URL: ${GITHUB_ISSUE_URL} + GH_TOKEN: ${GH_TOKEN} diff --git a/internal/scaffold/fullsend-repo/harness/explore.yaml b/internal/scaffold/fullsend-repo/harness/explore.yaml new file mode 100644 index 000000000..509262add --- /dev/null +++ b/internal/scaffold/fullsend-repo/harness/explore.yaml @@ -0,0 +1,55 @@ +--- +agent: agents/explore.md +doc: docs/agents/explore.md +model: opus +image: ghcr.io/fullsend-ai/fullsend-sandbox:latest +policy: policies/explore.yaml + +role: explore +slug: fullsend-ai-explore + +host_files: + - src: env/gcp-vertex.env + dest: /sandbox/workspace/.env.d/gcp-vertex.env + expand: true + - src: env/explore.env + dest: /sandbox/workspace/.env.d/explore.env + expand: true + - src: ${GOOGLE_APPLICATION_CREDENTIALS} + dest: /tmp/.gcp-credentials.json + - src: ${GCP_OIDC_TOKEN_FILE} + dest: /sandbox/workspace/.gcp-oidc-token + optional: true + - src: /tmp/workspace/issue-context.json + dest: /tmp/workspace/issue-context.json + optional: true + +skills: + - skills/public-research + - skills/jira-read + +pre_script: scripts/pre-explore.sh +post_script: scripts/post-explore.sh + +validation_loop: + script: scripts/validate-output-schema.sh + max_iterations: 2 + +runner_env: + ISSUE_KEY: "${ISSUE_KEY}" + ISSUE_SOURCE: "${ISSUE_SOURCE}" + REPO_FULL_NAME: "${REPO_FULL_NAME}" + GITHUB_ISSUE_NUMBER: "${GITHUB_ISSUE_NUMBER}" + GH_TOKEN: "${GH_TOKEN}" + AUTO_CREATE: "${AUTO_CREATE}" + FULLSEND_OUTPUT_SCHEMA: ${FULLSEND_DIR}/schemas/explore-result.schema.json + +timeout_minutes: 20 + +forge: + github: + pre_script: scripts/pre-explore.sh + post_script: scripts/post-explore.sh + runner_env: + GITHUB_ISSUE_URL: ${GITHUB_ISSUE_URL} + GH_TOKEN: ${GH_TOKEN} diff --git a/internal/scaffold/fullsend-repo/harness/refine.yaml b/internal/scaffold/fullsend-repo/harness/refine.yaml new file mode 100644 index 000000000..8da1e3457 --- /dev/null +++ b/internal/scaffold/fullsend-repo/harness/refine.yaml @@ -0,0 +1,64 @@ +--- +agent: agents/refine.md +doc: docs/agents/refine.md +model: opus +image: ghcr.io/fullsend-ai/fullsend-sandbox:latest +policy: policies/refine.yaml + +role: refine +slug: fullsend-ai-refine + +host_files: + - src: env/gcp-vertex.env + dest: /sandbox/workspace/.env.d/gcp-vertex.env + expand: true + - src: env/refine.env + dest: /sandbox/workspace/.env.d/refine.env + expand: true + - src: ${GOOGLE_APPLICATION_CREDENTIALS} + dest: /tmp/.gcp-credentials.json + - src: ${GCP_OIDC_TOKEN_FILE} + dest: /sandbox/workspace/.gcp-oidc-token + optional: true + - src: /tmp/workspace/issue-context.json + dest: /tmp/workspace/issue-context.json + optional: true + - src: /tmp/workspace/exploration_context.json + dest: /tmp/workspace/exploration_context.json + optional: true + - src: /tmp/workspace/critique-feedback.json + dest: /tmp/workspace/critique-feedback.json + optional: true + +skills: [] + +pre_script: scripts/pre-refine.sh +post_script: scripts/post-refine.sh + +validation_loop: + script: scripts/validate-output-schema.sh + max_iterations: 2 + +runner_env: + ISSUE_KEY: "${ISSUE_KEY}" + ISSUE_SOURCE: "${ISSUE_SOURCE}" + REPO_FULL_NAME: "${REPO_FULL_NAME}" + EXPLORE_RUN_ID: "${EXPLORE_RUN_ID}" + EXPLORE_CONTEXT_REF: "${EXPLORE_CONTEXT_REF}" + CRITIQUE_RUN_ID: "${CRITIQUE_RUN_ID}" + REVIEW_ROUND: "${REVIEW_ROUND}" + MAX_REVIEW_ROUNDS: "${MAX_REVIEW_ROUNDS}" + AUTO_CREATE: "${AUTO_CREATE}" + GITHUB_ISSUE_NUMBER: "${GITHUB_ISSUE_NUMBER}" + GH_TOKEN: "${GH_TOKEN}" + FULLSEND_OUTPUT_SCHEMA: ${FULLSEND_DIR}/schemas/refine-result.schema.json + +timeout_minutes: 25 + +forge: + github: + pre_script: scripts/pre-refine.sh + post_script: scripts/post-refine.sh + runner_env: + GITHUB_ISSUE_URL: ${GITHUB_ISSUE_URL} + GH_TOKEN: ${GH_TOKEN} diff --git a/internal/scaffold/fullsend-repo/policies/critique.yaml b/internal/scaffold/fullsend-repo/policies/critique.yaml new file mode 100644 index 000000000..e5d040834 --- /dev/null +++ b/internal/scaffold/fullsend-repo/policies/critique.yaml @@ -0,0 +1,54 @@ +--- +version: 1 + +filesystem_policy: + include_workdir: true + read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] + read_write: [/sandbox, /tmp, /dev/null] +landlock: + compatibility: best_effort +process: + run_as_user: sandbox + run_as_group: sandbox + +network_policies: + vertex_ai: + name: vertex-ai + endpoints: + - host: "api.anthropic.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-write + - host: "*.googleapis.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-write + binaries: + - path: "**/claude" + - path: "**/node" + + github_api: + name: github-api + endpoints: + - host: "api.github.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-write + - host: "github.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-write + - host: "raw.githubusercontent.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-only + binaries: + - path: "**/gh" + - path: "**/git" + - path: "**/node" + diff --git a/internal/scaffold/fullsend-repo/policies/explore.yaml b/internal/scaffold/fullsend-repo/policies/explore.yaml new file mode 100644 index 000000000..c57014197 --- /dev/null +++ b/internal/scaffold/fullsend-repo/policies/explore.yaml @@ -0,0 +1,71 @@ +--- +version: 1 + +filesystem_policy: + include_workdir: true + read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] + read_write: [/sandbox, /tmp, /dev/null] +landlock: + compatibility: best_effort +process: + run_as_user: sandbox + run_as_group: sandbox + +network_policies: + vertex_ai: + name: vertex-ai + endpoints: + - host: "api.anthropic.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-write + - host: "*.googleapis.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-write + binaries: + - path: "**/claude" + - path: "**/node" + + github_api: + name: github-api + endpoints: + - host: "api.github.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-write + - host: "github.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-write + - host: "raw.githubusercontent.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-only + binaries: + - path: "**/gh" + - path: "**/git" + - path: "**/node" + - path: "**/curl" + + web_search: + name: web-search + endpoints: + - host: "api.tavily.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-write + - host: "*.google.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-only + binaries: + - path: "**/curl" + - path: "**/node" diff --git a/internal/scaffold/fullsend-repo/policies/refine.yaml b/internal/scaffold/fullsend-repo/policies/refine.yaml new file mode 100644 index 000000000..e5d040834 --- /dev/null +++ b/internal/scaffold/fullsend-repo/policies/refine.yaml @@ -0,0 +1,54 @@ +--- +version: 1 + +filesystem_policy: + include_workdir: true + read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] + read_write: [/sandbox, /tmp, /dev/null] +landlock: + compatibility: best_effort +process: + run_as_user: sandbox + run_as_group: sandbox + +network_policies: + vertex_ai: + name: vertex-ai + endpoints: + - host: "api.anthropic.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-write + - host: "*.googleapis.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-write + binaries: + - path: "**/claude" + - path: "**/node" + + github_api: + name: github-api + endpoints: + - host: "api.github.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-write + - host: "github.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-write + - host: "raw.githubusercontent.com" + port: 443 + protocol: rest + enforcement: enforce + access: read-only + binaries: + - path: "**/gh" + - path: "**/git" + - path: "**/node" + diff --git a/internal/scaffold/fullsend-repo/schemas/critique-result.schema.json b/internal/scaffold/fullsend-repo/schemas/critique-result.schema.json new file mode 100644 index 000000000..5a3594681 --- /dev/null +++ b/internal/scaffold/fullsend-repo/schemas/critique-result.schema.json @@ -0,0 +1,135 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Critique Result", + "description": "Output schema for the critique agent — either an approval or revision request for a refinement plan.", + "type": "object", + "required": ["input", "verdict", "refinement_plan_summary", "review_round"], + "properties": { + "input": { + "type": "object", + "required": ["source", "key", "level", "summary"], + "properties": { + "source": { "type": "string", "enum": ["jira", "github", "text"] }, + "key": { "type": "string" }, + "level": { "type": "string", "enum": ["outcome", "feature", "epic", "story", "task", "issue"] }, + "summary": { "type": "string" } + } + }, + "verdict": { + "type": "string", + "enum": ["approved", "revise", "needs_input"] + }, + "question": { + "type": "object", + "description": "Required when verdict is needs_input. The ONE question that a human must answer.", + "properties": { + "dimension": { "type": "string" }, + "text": { "type": "string" }, + "impact": { "type": "string" }, + "what_refine_assumed": { "type": "string" } + }, + "required": ["dimension", "text", "impact"] + }, + "review_round": { + "type": "integer", + "minimum": 1 + }, + "refinement_plan_summary": { + "type": "object", + "required": ["epic_count", "story_count", "task_count", "total_children"], + "properties": { + "epic_count": { "type": "integer", "minimum": 0 }, + "story_count": { "type": "integer", "minimum": 0 }, + "task_count": { "type": "integer", "minimum": 0 }, + "total_children": { "type": "integer", "minimum": 0 } + } + }, + "assessment": { + "type": "object", + "properties": { + "coverage": { + "type": "object", + "properties": { + "score": { "type": "number", "minimum": 0, "maximum": 100 }, + "reasoning": { "type": "string" } + } + }, + "granularity": { + "type": "object", + "properties": { + "score": { "type": "number", "minimum": 0, "maximum": 100 }, + "reasoning": { "type": "string" } + } + }, + "dependency_coherence": { + "type": "object", + "properties": { + "score": { "type": "number", "minimum": 0, "maximum": 100 }, + "reasoning": { "type": "string" } + } + }, + "implementability": { + "type": "object", + "properties": { + "score": { "type": "number", "minimum": 0, "maximum": 100 }, + "reasoning": { "type": "string" } + } + }, + "scope_accuracy": { + "type": "object", + "properties": { + "score": { "type": "number", "minimum": 0, "maximum": 100 }, + "reasoning": { "type": "string" } + } + }, + "assumption_grounding": { + "type": "object", + "description": "Are the plan's architectural decisions backed by exploration context or unverified guesses?", + "properties": { + "score": { "type": "number", "minimum": 0, "maximum": 100 }, + "reasoning": { "type": "string" } + } + }, + "overall": { "type": "number", "minimum": 0, "maximum": 100 } + } + }, + "revisions": { + "type": "array", + "items": { + "type": "object", + "required": ["type", "target", "reasoning"], + "properties": { + "type": { + "type": "string", + "enum": ["remove", "merge", "split", "revise", "add"] + }, + "target": { + "type": "string", + "description": "Title of the child item this revision targets, or 'plan' for plan-level feedback" + }, + "reasoning": { "type": "string" }, + "suggestion": { "type": "string" } + } + } + }, + "comment": { "type": "string", "maxLength": 4000 }, + "summary": { "type": "string", "maxLength": 2000 } + }, + "allOf": [ + { + "if": { "properties": { "verdict": { "const": "revise" } } }, + "then": { + "required": ["input", "verdict", "refinement_plan_summary", "review_round", "revisions", "comment", "summary"], + "properties": { "revisions": { "minItems": 1 } } + } + }, + { + "if": { "properties": { "verdict": { "const": "needs_input" } } }, + "then": { "required": ["input", "verdict", "refinement_plan_summary", "review_round", "question", "comment", "summary"] } + }, + { + "if": { "properties": { "verdict": { "const": "approved" } } }, + "then": { "required": ["input", "verdict", "refinement_plan_summary", "review_round", "assessment", "comment", "summary"] } + } + ] +} diff --git a/internal/scaffold/fullsend-repo/schemas/explore-result.schema.json b/internal/scaffold/fullsend-repo/schemas/explore-result.schema.json new file mode 100644 index 000000000..6e4662cea --- /dev/null +++ b/internal/scaffold/fullsend-repo/schemas/explore-result.schema.json @@ -0,0 +1,97 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Exploration Result", + "description": "Output schema for the explore agent — public research context for a work item.", + "type": "object", + "required": ["input", "technical_landscape", "related_work", "confidence", "summary"], + "properties": { + "input": { + "type": "object", + "required": ["source", "key", "level", "summary"], + "properties": { + "source": { "type": "string", "enum": ["jira", "github", "text", "web"] }, + "key": { "type": "string" }, + "level": { "type": "string", "enum": ["outcome", "feature", "epic", "story", "task", "issue"] }, + "summary": { "type": "string" } + } + }, + "technical_landscape": { + "type": "object", + "properties": { + "languages": { "type": "array", "items": { "type": "string" } }, + "frameworks": { "type": "array", "items": { "type": "string" } }, + "build_system": { "type": "string" }, + "deployment_targets": { "type": "array", "items": { "type": "string" } }, + "key_dependencies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "version": { "type": "string" }, + "role": { "type": "string" } + } + } + }, + "existing_patterns": { "type": "array", "items": { "type": "string" } }, + "api_surface": { "type": "array", "items": { "type": "string" } }, + "test_infrastructure": { "type": "string" } + } + }, + "related_work": { + "type": "array", + "items": { + "type": "object", + "required": ["type", "source", "key", "title", "relevance"], + "properties": { + "type": { "type": "string", "enum": ["issue", "pr", "discussion", "adr", "documentation", "test", "rfc", "blog", "commit", "release", "other"] }, + "source": { "type": "string" }, + "key": { "type": "string" }, + "title": { "type": "string" }, + "state": { "type": "string" }, + "relevance": { "type": "string" } + } + } + }, + "architectural_constraints": { + "type": "array", + "items": { "type": "string" } + }, + "competitive_context": { + "type": "array", + "items": { + "type": "object", + "properties": { + "alternative": { "type": "string" }, + "approach": { "type": "string" }, + "source_url": { "type": "string" } + } + } + }, + "gaps": { + "type": "array", + "items": { + "type": "object", + "required": ["dimension", "description"], + "properties": { + "dimension": { "type": "string" }, + "description": { "type": "string" }, + "impact": { "type": "string" } + } + } + }, + "confidence": { + "type": "object", + "required": ["overall"], + "properties": { + "technical_landscape": { "type": "number", "minimum": 0, "maximum": 100 }, + "related_work": { "type": "number", "minimum": 0, "maximum": 100 }, + "architectural_constraints": { "type": "number", "minimum": 0, "maximum": 100 }, + "competitive_context": { "type": "number", "minimum": 0, "maximum": 100 }, + "requirements_clarity": { "type": "number", "minimum": 0, "maximum": 100 }, + "overall": { "type": "number", "minimum": 0, "maximum": 100 } + } + }, + "summary": { "type": "string", "maxLength": 1000 } + } +} diff --git a/internal/scaffold/fullsend-repo/schemas/refine-result.schema.json b/internal/scaffold/fullsend-repo/schemas/refine-result.schema.json new file mode 100644 index 000000000..b2483f2fa --- /dev/null +++ b/internal/scaffold/fullsend-repo/schemas/refine-result.schema.json @@ -0,0 +1,94 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Refinement Result", + "description": "Output schema for the refine agent — always a complete decomposition plan.", + "type": "object", + "required": ["input", "status", "confidence", "target_level", "children", "comment", "summary"], + "properties": { + "input": { + "type": "object", + "required": ["source", "key", "level", "summary"], + "properties": { + "source": { "type": "string", "enum": ["jira", "github", "text"] }, + "key": { "type": "string" }, + "level": { "type": "string", "enum": ["outcome", "feature", "epic", "story", "task", "issue"] }, + "summary": { "type": "string" } + } + }, + "status": { + "type": "string", + "enum": ["complete"] + }, + "target_level": { + "type": "string", + "enum": ["feature", "epic", "story", "task", "sub-issue"] + }, + "confidence": { + "type": "object", + "required": ["overall"], + "properties": { + "scope_clarity": { "type": "number", "minimum": 0, "maximum": 100 }, + "technical_grounding": { "type": "number", "minimum": 0, "maximum": 100 }, + "acceptance_criteria": { "type": "number", "minimum": 0, "maximum": 100 }, + "dependencies": { "type": "number", "minimum": 0, "maximum": 100 }, + "sizing": { "type": "number", "minimum": 0, "maximum": 100 }, + "overall": { "type": "number", "minimum": 0, "maximum": 100 } + } + }, + "children": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["title", "type", "description", "acceptance_criteria"], + "properties": { + "title": { "type": "string" }, + "type": { "type": "string", "description": "Issue type matching the project's available types (lowercase). Common: feature, epic, story, task, sub-issue, bug, spike." }, + "parent_title": { "type": ["string", "null"] }, + "description": { "type": "string" }, + "acceptance_criteria": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1 + }, + "dependencies": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { "type": "string", "enum": ["blocks", "blocked_by", "related"] }, + "target": { "type": "string" }, + "description": { "type": "string" } + } + } + }, + "labels": { "type": "array", "items": { "type": "string" } }, + "priority": { "type": "string", "enum": ["highest", "critical", "high", "medium", "low"] }, + "estimated_scope": { "type": "string", "enum": ["S", "M", "L", "XL"] }, + "confidence": { "type": "number", "minimum": 0, "maximum": 100 }, + "deployment_target": { "type": "string" } + } + } + }, + "open_questions": { + "type": "array", + "items": { + "type": "object", + "required": ["dimension", "question", "impact"], + "properties": { + "dimension": { "type": "string" }, + "question": { "type": "string" }, + "impact": { "type": "string" }, + "assumption_used": { "type": "string" } + } + } + }, + "proposed_description": { "type": "string", "maxLength": 20000 }, + "dimensions_covered": { "type": "array", "items": { "type": "string" } }, + "dimensions_missing": { "type": "array", "items": { "type": "string" } }, + "uncited_assumptions": { "type": "array", "items": { "type": "string" } }, + "deployment_targets": { "type": "array", "items": { "type": "string" } }, + "comment": { "type": "string", "maxLength": 4000 }, + "summary": { "type": "string", "maxLength": 2000 } + } +} diff --git a/internal/scaffold/fullsend-repo/scripts/create-children-test.sh b/internal/scaffold/fullsend-repo/scripts/create-children-test.sh new file mode 100755 index 000000000..8aebbb7a7 --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/create-children-test.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# create-children-test.sh — Test create-children.sh issue creation logic. +# +# Run from the repo root: bash internal/scaffold/fullsend-repo/scripts/create-children-test.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CREATE_SCRIPT="${SCRIPT_DIR}/create-children.sh" +FAILURES=0 + +TEST_TMPDIR="$(mktemp -d)" +trap 'rm -rf "${TEST_TMPDIR}"' EXIT + +GH_LOG="${TEST_TMPDIR}/gh-calls.log" +MOCK_BIN="${TEST_TMPDIR}/bin" +ISSUE_COUNTER_FILE="${TEST_TMPDIR}/issue-counter" +mkdir -p "${MOCK_BIN}" + +echo "100" > "${ISSUE_COUNTER_FILE}" + +cat > "${MOCK_BIN}/gh" <> "${GH_LOG}" + +case "\$*" in + *"label create"*) + exit 0 + ;; + *"issue create"*) + cat > /dev/null # consume --body-file stdin + counter=\$(cat "${ISSUE_COUNTER_FILE}") + counter=\$((counter + 1)) + echo "\$counter" > "${ISSUE_COUNTER_FILE}" + echo "https://github.com/test-org/test-repo/issues/\${counter}" + exit 0 + ;; + *"repos/"*"/issues/"*"--jq"*) + echo "I_node_id_123" + exit 0 + ;; + *"repos/"*"/issues/"*) + echo '{"id": "I_node_id_123"}' + exit 0 + ;; + *"sub_issues"*) + exit 0 + ;; +esac +exit 0 +MOCKEOF +chmod +x "${MOCK_BIN}/gh" + +cat > "${MOCK_BIN}/python3" <<'MOCKEOF' +#!/usr/bin/env bash +if [[ "${1:-}" == "-c" ]]; then + if [[ "${2:-}" == *"time.time"* ]]; then + echo "1000000" + exit 0 + fi + echo "" + exit 0 +fi +exec /usr/bin/python3 "$@" +MOCKEOF +chmod +x "${MOCK_BIN}/python3" + +export PATH="${MOCK_BIN}:${PATH}" +export GH_LOG="${GH_LOG}" +export GH_TOKEN="fake-token" +export ISSUE_KEY="42" +export ISSUE_SOURCE="github" +export REPO_FULL_NAME="test-org/test-repo" +export GITHUB_ISSUE_NUMBER="42" + +FLAT_CHILDREN_FIXTURE='{ + "status": "complete", + "children": [ + {"title": "Child A", "type": "story", "description": "First child", "acceptance_criteria": ["AC1"], "priority": "high", "estimated_scope": "S"}, + {"title": "Child B", "type": "task", "description": "Second child", "acceptance_criteria": ["AC2"], "priority": "medium", "estimated_scope": "M"} + ] +}' + +HIERARCHICAL_FIXTURE='{ + "status": "complete", + "children": [ + {"title": "Epic Parent", "type": "epic", "description": "Parent epic", "acceptance_criteria": ["AC-E1"]}, + {"title": "Story under Epic", "type": "story", "parent_title": "Epic Parent", "description": "Child story", "acceptance_criteria": ["AC-S1"]}, + {"title": "Task under Story", "type": "task", "parent_title": "Story under Epic", "description": "Grandchild task", "acceptance_criteria": ["AC-T1"]} + ] +}' + +ORPHAN_FIXTURE='{ + "status": "complete", + "children": [ + {"title": "Good Child", "type": "story", "description": "Has no parent ref", "acceptance_criteria": ["AC1"]}, + {"title": "Orphan", "type": "task", "parent_title": "Nonexistent Parent", "description": "Bad ref", "acceptance_criteria": ["AC2"]} + ] +}' + +run_test() { + local test_name="$1" + local fixture="$2" + local expect_failure="${3:-false}" + + local result_file="${TEST_TMPDIR}/result-${test_name}.json" + echo "${fixture}" > "${result_file}" + + echo "100" > "${ISSUE_COUNTER_FILE}" + : > "${GH_LOG}" + + local exit_code=0 + RESULT_FILE="${result_file}" bash "${CREATE_SCRIPT}" > "${TEST_TMPDIR}/stdout-${test_name}.log" 2>&1 || exit_code=$? + + if [[ "${expect_failure}" == "true" ]]; then + if [[ ${exit_code} -eq 0 ]]; then + echo "FAIL: ${test_name} — expected failure but got success" + FAILURES=$((FAILURES + 1)) + return + fi + echo "PASS: ${test_name} (expected failure)" + return + fi + + if [[ ${exit_code} -ne 0 ]]; then + echo "FAIL: ${test_name} — exit code ${exit_code}" + cat "${TEST_TMPDIR}/stdout-${test_name}.log" + FAILURES=$((FAILURES + 1)) + return + fi + + echo "PASS: ${test_name}" +} + +assert_gh_called() { + local test_name="$1" pattern="$2" + if ! grep -qF "${pattern}" "${GH_LOG}"; then + echo "FAIL: ${test_name} — expected gh call matching '${pattern}'" + cat "${GH_LOG}" + FAILURES=$((FAILURES + 1)) + fi +} + +assert_stdout_contains() { + local test_name="$1" pattern="$2" + if ! grep -qF "${pattern}" "${TEST_TMPDIR}/stdout-${test_name}.log"; then + echo "FAIL: ${test_name} — expected stdout containing '${pattern}'" + FAILURES=$((FAILURES + 1)) + fi +} + +count_gh_calls() { + local pattern="$1" + grep -cF "${pattern}" "${GH_LOG}" 2>/dev/null || echo "0" +} + +# --- Tests --- + +# 1. Flat children — 2 issues created under root parent +run_test "flat-children" "$FLAT_CHILDREN_FIXTURE" +ISSUE_CREATES=$(count_gh_calls "issue create") +if [[ "$ISSUE_CREATES" == "2" ]]; then + echo "PASS: flat-children created 2 issues" +else + echo "FAIL: flat-children — expected 2 issue creates, got ${ISSUE_CREATES}" + FAILURES=$((FAILURES + 1)) +fi + +# 2. Hierarchical children — topological ordering +run_test "hierarchical" "$HIERARCHICAL_FIXTURE" +assert_stdout_contains "hierarchical" "Created" +ISSUE_CREATES=$(count_gh_calls "issue create") +if [[ "$ISSUE_CREATES" == "3" ]]; then + echo "PASS: hierarchical created 3 issues" +else + echo "FAIL: hierarchical — expected 3 issue creates, got ${ISSUE_CREATES}" + FAILURES=$((FAILURES + 1)) +fi + +# 3. Orphan fallback — unresolvable parent_title falls back to root +run_test "orphan-fallback" "$ORPHAN_FIXTURE" +assert_stdout_contains "orphan-fallback" "orphan" +ISSUE_CREATES=$(count_gh_calls "issue create") +if [[ "$ISSUE_CREATES" == "2" ]]; then + echo "PASS: orphan-fallback created 2 issues" +else + echo "FAIL: orphan-fallback — expected 2 issue creates, got ${ISSUE_CREATES}" + FAILURES=$((FAILURES + 1)) +fi + +# 4. Missing RESULT_FILE +unset RESULT_FILE +run_test "missing-result-file" "unused" "true" +export RESULT_FILE="/nonexistent" +run_test "nonexistent-result-file" "unused" "true" + +# 5. Invalid JSON +run_test "invalid-json" "not valid json" "true" + +if [[ ${FAILURES} -gt 0 ]]; then + echo "" + echo "${FAILURES} test(s) failed." + exit 1 +fi + +echo "" +echo "All create-children tests passed." diff --git a/internal/scaffold/fullsend-repo/scripts/create-children.sh b/internal/scaffold/fullsend-repo/scripts/create-children.sh new file mode 100755 index 000000000..579dfdc09 --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/create-children.sh @@ -0,0 +1,378 @@ +#!/usr/bin/env bash +# create-children.sh — Create child issues from an approved refinement plan. +# +# Reusable script that reads a refinement result JSON and creates child issues +# in topological order using parent_title references for hierarchy. +# +# Can be called from: +# - post-critique.sh (auto-approval path) +# - create-children.yml workflow (human-approval path) +# +# Required env vars: +# RESULT_FILE — Path to the approved agent-result.json +# ISSUE_KEY — Parent issue identifier (Jira key or GH issue number) +# ISSUE_SOURCE — "jira" or "github" +# GH_TOKEN — GitHub token +# +# GitHub flow env vars: +# GITHUB_ISSUE_NUMBER — GitHub issue number +# REPO_FULL_NAME — owner/repo +# PUSH_TOKEN — Token with write access +# +# Jira flow env vars: +# JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [[ -f "${SCRIPT_DIR}/pipeline-events.sh" ]]; then + source "${SCRIPT_DIR}/pipeline-events.sh" + HAS_PE=true +else + HAS_PE=false + pe_start() { :; } + pe_end() { :; } +fi + +if [[ -z "${RESULT_FILE:-}" ]]; then + echo "ERROR: RESULT_FILE env var not set" + exit 1 +fi + +if [[ ! -f "${RESULT_FILE}" ]]; then + echo "ERROR: Result file not found: ${RESULT_FILE}" + exit 1 +fi + +if ! jq empty "${RESULT_FILE}" 2>/dev/null; then + echo "ERROR: ${RESULT_FILE} is not valid JSON" + exit 1 +fi + +USE_GITHUB=false +if [[ -n "${GITHUB_ISSUE_NUMBER:-}" && "${GITHUB_ISSUE_NUMBER}" != "" && "${GITHUB_ISSUE_NUMBER}" != "N/A" ]]; then + USE_GITHUB=true +elif [[ "${ISSUE_SOURCE:-}" == "github" ]]; then + USE_GITHUB=true + GITHUB_ISSUE_NUMBER="${ISSUE_KEY}" +fi + +# --- Helper functions --- + +github_create_issue() { + local repo="$1" title="$2" body="$3" labels="$4" parent_number="${5:-}" + local args=(--repo "$repo" --title "$title") + if [[ -n "$labels" && "$labels" != "null" ]]; then + while IFS= read -r label; do + if [[ -n "$label" ]]; then + gh label create "$label" --repo "$repo" --force 2>/dev/null || true + args+=(--label "$label") + fi + done < <(echo "$labels" | jq -r '.[]') + fi + local result + result=$(printf '%s' "$body" | gh issue create "${args[@]}" --body-file - 2>&1) || { + echo "::warning::Failed to create issue '${title}': ${result}" >&2 + echo "FAILED" + return 0 + } + + local issue_number + issue_number=$(echo "$result" | grep -oP '/issues/\K[0-9]+' || true) + + if [[ -n "$parent_number" && -n "$issue_number" ]]; then + local child_id + child_id=$(gh api "repos/${repo}/issues/${issue_number}" --jq '.id' 2>/dev/null) + if [[ -n "$child_id" ]]; then + gh api "repos/${repo}/issues/${parent_number}/sub_issues" \ + -F sub_issue_id="$child_id" \ + --silent 2>/dev/null || \ + echo "::warning::Could not link #${issue_number} as sub-issue of #${parent_number}" >&2 + fi + fi + + echo "$issue_number" +} + +jira_create_issue() { + local project="$1" type="$2" summary="$3" description="$4" parent_key="${5:-}" + local auth + auth=$(printf '%s:%s' "$JIRA_EMAIL" "$JIRA_API_TOKEN" | base64 -w0) + + # Build ADF description: split on double-newlines into paragraphs, + # single newlines become hardBreak nodes within a paragraph. + local adf_desc + adf_desc=$(python3 -c " +import json, sys + +text = sys.argv[1] +paragraphs = text.split('\n\n') +content = [] +for para in paragraphs: + para = para.strip() + if not para: + continue + # Split single newlines into text + hardBreak sequences + lines = para.split('\n') + inline_content = [] + for i, line in enumerate(lines): + if line.strip(): + inline_content.append({'type': 'text', 'text': line}) + if i < len(lines) - 1: + inline_content.append({'type': 'hardBreak'}) + if inline_content: + content.append({'type': 'paragraph', 'content': inline_content}) + +doc = {'type': 'doc', 'version': 1, 'content': content if content else [{'type': 'paragraph', 'content': [{'type': 'text', 'text': ' '}]}]} +print(json.dumps(doc)) +" "$description") + + local payload + payload=$(jq -n \ + --arg proj "$project" \ + --arg type "$type" \ + --arg summary "$summary" \ + --argjson desc "$adf_desc" \ + --arg parent "$parent_key" \ + '{ + fields: ({ + project: {key: $proj}, + issuetype: {name: $type}, + summary: $summary, + description: $desc + } + (if $parent != "" then {parent: {key: $parent}} else {} end)) + }') + + local response http_code + response=$(curl -sS -w "\n%{http_code}" -X POST \ + -H "Authorization: Basic $auth" \ + -H "Content-Type: application/json" \ + -d "$payload" \ + "https://${JIRA_HOST}/rest/api/3/issue") + + http_code=$(echo "$response" | tail -1) + local body + body=$(echo "$response" | sed '$d') + + if [[ "$http_code" -ge 400 ]]; then + echo "::warning::Jira API returned ${http_code} creating '${summary}' (type: ${type}, parent: ${parent_key}): ${body}" >&2 + # If parent hierarchy fails, retry without parent + if [[ -n "$parent_key" && "$http_code" == "400" ]]; then + echo " Retrying without parent..." >&2 + payload=$(jq -n \ + --arg proj "$project" \ + --arg type "$type" \ + --arg summary "$summary" \ + --argjson desc "$adf_desc" \ + '{ + fields: { + project: {key: $proj}, + issuetype: {name: $type}, + summary: $summary, + description: $desc + } + }') + response=$(curl -sS -w "\n%{http_code}" -X POST \ + -H "Authorization: Basic $auth" \ + -H "Content-Type: application/json" \ + -d "$payload" \ + "https://${JIRA_HOST}/rest/api/3/issue") + http_code=$(echo "$response" | tail -1) + body=$(echo "$response" | sed '$d') + if [[ "$http_code" -ge 400 ]]; then + echo "::warning::Retry without parent also failed (${http_code}): ${body}" >&2 + echo "" + return 0 + fi + else + echo "" + return 0 + fi + fi + + echo "$body" | jq -r '.key' +} + +resolve_jira_type() { + local requested_type="$1" + local available_types="${2:-}" + + if [[ -z "$available_types" || "$available_types" == "[]" ]]; then + case "${requested_type,,}" in + feature) echo "Feature" ;; + epic) echo "Epic" ;; + story) echo "Story" ;; + task) echo "Task" ;; + spike) echo "Task" ;; + bug) echo "Bug" ;; + *) echo "Story" ;; + esac + return + fi + + local match + match=$(echo "$available_types" | jq -r --arg t "$requested_type" \ + '[.[].name] | map(select(ascii_downcase == ($t | ascii_downcase))) | .[0] // empty') + + if [[ -n "$match" ]]; then + echo "$match" + return + fi + + local fallback + fallback=$(echo "$available_types" | jq -r ' + [.[] | select(.subtask != true) | .name] | + if any(. == "Story") then "Story" + elif any(. == "Task") then "Task" + elif any(. == "Bug") then "Bug" + else .[0] // "Story" + end') + + echo "$fallback" +} + +# Load available issue types from issue context if present +AVAILABLE_TYPES="[]" +ISSUE_CONTEXT_FILE="/tmp/workspace/issue-context.json" +if [[ -f "$ISSUE_CONTEXT_FILE" ]]; then + AVAILABLE_TYPES=$(jq -c '.project.available_issue_types // []' "$ISSUE_CONTEXT_FILE") +fi + +# --- Create children in topological order --- + +pe_start "create-children" "create-children" + +CHILD_COUNT=$(jq '.children | length' "${RESULT_FILE}") +echo "Creating ${CHILD_COUNT} child issue(s) with hierarchy..." + +declare -A TITLE_TO_KEY +CREATED_KEYS=() +CREATED_COUNT=0 +MAX_PASSES=5 +PASS=0 + +declare -A CREATED_IDX + +while [[ $CREATED_COUNT -lt $CHILD_COUNT && $PASS -lt $MAX_PASSES ]]; do + PASS=$((PASS + 1)) + PROGRESS=false + + for i in $(seq 0 $((CHILD_COUNT - 1))); do + if [[ -n "${CREATED_IDX[$i]:-}" ]]; then continue; fi + + CHILD_TITLE=$(jq -r ".children[${i}].title" "${RESULT_FILE}") + CHILD_PARENT_TITLE=$(jq -r ".children[${i}].parent_title // \"\"" "${RESULT_FILE}") + CHILD_TYPE=$(jq -r ".children[${i}].type" "${RESULT_FILE}") + CHILD_DESC=$(jq -r ".children[${i}].description" "${RESULT_FILE}") + CHILD_AC=$(jq -r ".children[${i}].acceptance_criteria | map(\"- [ ] \" + .) | join(\"\n\")" "${RESULT_FILE}") + CHILD_LABELS=$(jq -c ".children[${i}].labels // []" "${RESULT_FILE}") + CHILD_PRIORITY=$(jq -r ".children[${i}].priority // \"medium\"" "${RESULT_FILE}") + CHILD_SCOPE=$(jq -r ".children[${i}].estimated_scope // \"M\"" "${RESULT_FILE}") + + PARENT_KEY_FOR_CHILD="" + if [[ -z "$CHILD_PARENT_TITLE" || "$CHILD_PARENT_TITLE" == "null" ]]; then + PARENT_KEY_FOR_CHILD="$ISSUE_KEY" + elif [[ -n "${TITLE_TO_KEY[$CHILD_PARENT_TITLE]:-}" ]]; then + PARENT_KEY_FOR_CHILD="${TITLE_TO_KEY[$CHILD_PARENT_TITLE]}" + else + continue + fi + + FULL_BODY="${CHILD_DESC} + +## Acceptance Criteria + +${CHILD_AC} + +--- +*Priority: ${CHILD_PRIORITY} | Scope: ${CHILD_SCOPE} | Generated by fullsend refine agent*" + + if $USE_GITHUB; then + TYPE_LABEL="$CHILD_TYPE" + COMBINED_LABELS=$(echo "$CHILD_LABELS" | jq --arg t "$TYPE_LABEL" '. + [$t]') + NEW_ISSUE=$(github_create_issue "${REPO_FULL_NAME}" "$CHILD_TITLE" "$FULL_BODY" "$COMBINED_LABELS" "$PARENT_KEY_FOR_CHILD") + if [[ -z "$NEW_ISSUE" || "$NEW_ISSUE" == "FAILED" ]]; then + echo " [pass ${PASS}] FAILED to create ${CHILD_TYPE}: ${CHILD_TITLE}" + continue + fi + echo " [pass ${PASS}] Created ${CHILD_TYPE} #${NEW_ISSUE} under #${PARENT_KEY_FOR_CHILD}" + TITLE_TO_KEY["$CHILD_TITLE"]="$NEW_ISSUE" + CREATED_KEYS+=("#$NEW_ISSUE") + else + PROJECT_KEY=$(echo "$ISSUE_KEY" | sed 's/-.*//') + JIRA_TYPE=$(resolve_jira_type "$CHILD_TYPE" "$AVAILABLE_TYPES") + NEW_KEY=$(jira_create_issue "$PROJECT_KEY" "$JIRA_TYPE" "$CHILD_TITLE" "$FULL_BODY" "$PARENT_KEY_FOR_CHILD") + if [[ -z "$NEW_KEY" ]]; then + echo " [pass ${PASS}] FAILED to create ${JIRA_TYPE}: ${CHILD_TITLE}" + continue + fi + echo " [pass ${PASS}] Created ${JIRA_TYPE} ${NEW_KEY} under ${PARENT_KEY_FOR_CHILD} (requested: ${CHILD_TYPE})" + TITLE_TO_KEY["$CHILD_TITLE"]="$NEW_KEY" + CREATED_KEYS+=("$NEW_KEY") + fi + + CREATED_IDX[$i]=1 + CREATED_COUNT=$((CREATED_COUNT + 1)) + PROGRESS=true + done + + if ! $PROGRESS; then + echo "::warning::Pass ${PASS} made no progress — $((CHILD_COUNT - CREATED_COUNT)) items have unresolvable parent_title references" + break + fi +done + +# Orphans fall back to root parent +if [[ $CREATED_COUNT -lt $CHILD_COUNT ]]; then + echo "::warning::Creating remaining orphaned items under root issue" + for i in $(seq 0 $((CHILD_COUNT - 1))); do + if [[ -n "${CREATED_IDX[$i]:-}" ]]; then continue; fi + + CHILD_TITLE=$(jq -r ".children[${i}].title" "${RESULT_FILE}") + CHILD_TYPE=$(jq -r ".children[${i}].type" "${RESULT_FILE}") + CHILD_DESC=$(jq -r ".children[${i}].description" "${RESULT_FILE}") + CHILD_AC=$(jq -r ".children[${i}].acceptance_criteria | map(\"- [ ] \" + .) | join(\"\n\")" "${RESULT_FILE}") + CHILD_LABELS=$(jq -c ".children[${i}].labels // []" "${RESULT_FILE}") + CHILD_PRIORITY=$(jq -r ".children[${i}].priority // \"medium\"" "${RESULT_FILE}") + CHILD_SCOPE=$(jq -r ".children[${i}].estimated_scope // \"M\"" "${RESULT_FILE}") + + FULL_BODY="${CHILD_DESC} + +## Acceptance Criteria + +${CHILD_AC} + +--- +*Priority: ${CHILD_PRIORITY} | Scope: ${CHILD_SCOPE} | Generated by fullsend refine agent*" + + if $USE_GITHUB; then + TYPE_LABEL="$CHILD_TYPE" + COMBINED_LABELS=$(echo "$CHILD_LABELS" | jq --arg t "$TYPE_LABEL" '. + [$t]') + NEW_ISSUE=$(github_create_issue "${REPO_FULL_NAME}" "$CHILD_TITLE" "$FULL_BODY" "$COMBINED_LABELS" "$ISSUE_KEY") + if [[ -z "$NEW_ISSUE" || "$NEW_ISSUE" == "FAILED" ]]; then + echo " [orphan] FAILED to create: ${CHILD_TITLE}" + continue + fi + echo " [orphan] Created #${NEW_ISSUE} under #${ISSUE_KEY}" + CREATED_KEYS+=("#$NEW_ISSUE") + else + PROJECT_KEY=$(echo "$ISSUE_KEY" | sed 's/-.*//') + JIRA_TYPE=$(resolve_jira_type "$CHILD_TYPE" "$AVAILABLE_TYPES") + NEW_KEY=$(jira_create_issue "$PROJECT_KEY" "$JIRA_TYPE" "$CHILD_TITLE" "$FULL_BODY" "$ISSUE_KEY") + if [[ -z "$NEW_KEY" ]]; then + echo " [orphan] FAILED to create ${JIRA_TYPE}: ${CHILD_TITLE}" + continue + fi + echo " [orphan] Created ${JIRA_TYPE}: ${NEW_KEY} (requested: ${CHILD_TYPE})" + CREATED_KEYS+=("$NEW_KEY") + fi + done +fi + +pe_end "create-children" "create-children" "$(jq -nc --argjson total "$CHILD_COUNT" --argjson created "${#CREATED_KEYS[@]}" --argjson orphaned "$((CHILD_COUNT - CREATED_COUNT))" '{total:$total, created:$created, orphaned:$orphaned}')" + +echo "::notice::Created ${#CREATED_KEYS[@]} child issue(s): ${CREATED_KEYS[*]}" + +# Export for callers that need the result +export CREATED_CHILD_COUNT="${#CREATED_KEYS[@]}" +export CREATED_CHILD_KEYS="${CREATED_KEYS[*]}" diff --git a/internal/scaffold/fullsend-repo/scripts/markdown-to-adf.py b/internal/scaffold/fullsend-repo/scripts/markdown-to-adf.py new file mode 100755 index 000000000..595e0ec28 --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/markdown-to-adf.py @@ -0,0 +1,242 @@ +#!/usr/bin/env python3 +"""Convert markdown-style text to Jira Atlassian Document Format (ADF). + +Usage: + echo "## Heading\n\nSome **bold** text" | python3 markdown-to-adf.py + python3 markdown-to-adf.py < comment.md + +Outputs a JSON object suitable for the Jira REST API comment body field. +Handles: headings, bold, code, links, bullet lists, horizontal rules, paragraphs. +""" +import json +import re +import sys +from urllib.parse import urlparse + +MAX_INPUT_BYTES = 128 * 1024 +ALLOWED_SCHEMES = {"http", "https", "mailto", ""} + + +def parse_inline(text: str) -> list: + """Parse inline markdown (bold, code, links) into ADF inline nodes. + + The input must be a single line — newlines are not permitted in ADF text + nodes. Use parse_inline_multiline() for text that may span lines. + """ + nodes = [] + pos = 0 + pattern = re.compile( + r'(?P\*\*(.+?)\*\*)' + r'|(?P`([^`]+)`)' + r'|(?P\[([^\]]+)\]\(([^)]+)\))' + ) + for m in pattern.finditer(text): + if m.start() > pos: + plain = text[pos:m.start()] + if plain: + nodes.append({"type": "text", "text": plain}) + if m.group("bold"): + nodes.append({ + "type": "text", + "text": m.group(2), + "marks": [{"type": "strong"}], + }) + elif m.group("code"): + nodes.append({ + "type": "text", + "text": m.group(4), + "marks": [{"type": "code"}], + }) + elif m.group("link"): + href = m.group(7) + scheme = urlparse(href).scheme.lower() + if scheme in ALLOWED_SCHEMES: + nodes.append({ + "type": "text", + "text": m.group(6), + "marks": [{"type": "link", "attrs": {"href": href}}], + }) + else: + nodes.append({"type": "text", "text": f"{m.group(6)} ({href})"}) + pos = m.end() + if pos < len(text): + remainder = text[pos:] + if remainder: + nodes.append({"type": "text", "text": remainder}) + if not nodes and text: + nodes.append({"type": "text", "text": text}) + return nodes + + +def parse_inline_multiline(text: str) -> list: + """Parse inline markdown, inserting hardBreak nodes for newlines. + + Jira's ADF validator rejects literal newline characters inside text nodes. + This function splits on newlines and inserts {"type": "hardBreak"} between + line segments so the output is valid ADF. + """ + lines = text.split("\n") + nodes: list = [] + for i, line in enumerate(lines): + if line: + nodes.extend(parse_inline(line)) + if i < len(lines) - 1: + nodes.append({"type": "hardBreak"}) + # Strip trailing hardBreaks (from trailing empty lines) + while nodes and nodes[-1].get("type") == "hardBreak": + nodes.pop() + return nodes if nodes else [{"type": "text", "text": " "}] + + +def text_to_adf(text: str) -> dict: + """Convert markdown-style text to an ADF document.""" + doc = {"type": "doc", "version": 1, "content": []} + blocks = re.split(r'\n{2,}', text.strip()) + + for block in blocks: + block = block.strip() + if not block: + continue + + if block == "---": + doc["content"].append({"type": "rule"}) + continue + + heading_match = re.match(r'^(#{1,6})\s+(.+)$', block) + if heading_match: + level = len(heading_match.group(1)) + doc["content"].append({ + "type": "heading", + "attrs": {"level": level}, + "content": parse_inline(heading_match.group(2)), + }) + continue + + lines = block.split("\n") + + if all(re.match(r'^\s*[-*]\s+', line) for line in lines if line.strip()): + list_node = {"type": "bulletList", "content": []} + for line in lines: + item_text = re.sub(r'^\s*[-*]\s+', '', line).strip() + if item_text: + list_node["content"].append({ + "type": "listItem", + "content": [{"type": "paragraph", "content": parse_inline(item_text)}], + }) + if list_node["content"]: + doc["content"].append(list_node) + continue + + if all(re.match(r'^\s*\d+[.)]\s+', line) for line in lines if line.strip()): + list_node = {"type": "orderedList", "content": []} + for line in lines: + item_text = re.sub(r'^\s*\d+[.)]\s+', '', line).strip() + if item_text: + list_node["content"].append({ + "type": "listItem", + "content": [{"type": "paragraph", "content": parse_inline(item_text)}], + }) + if list_node["content"]: + doc["content"].append(list_node) + continue + + table_match = re.match(r'^\|', block) + if table_match: + table_lines = [l for l in lines if l.strip() and not re.match(r'^\|[-\s|]+\|$', l)] + if len(table_lines) >= 1: + table_node = {"type": "table", "attrs": {"layout": "default"}, "content": []} + for i, tl in enumerate(table_lines): + cells = [c.strip() for c in tl.strip('|').split('|')] + cell_type = "tableHeader" if i == 0 else "tableCell" + row = {"type": "tableRow", "content": []} + for cell in cells: + row["content"].append({ + "type": cell_type, + "content": [{"type": "paragraph", "content": parse_inline(cell)}], + }) + table_node["content"].append(row) + doc["content"].append(table_node) + continue + + mixed_content = [] + in_list = False + list_type = "bulletList" + list_items = [] + + def _flush_list(): + nonlocal list_items, in_list, list_type + if list_items: + ln = {"type": list_type, "content": []} + for it in list_items: + ln["content"].append({"type": "listItem", "content": [{"type": "paragraph", "content": parse_inline(it)}]}) + doc["content"].append(ln) + list_items = [] + in_list = False + list_type = "bulletList" + + for line in lines: + is_bullet = bool(re.match(r'^\s*[-*]\s+', line)) + is_ordered = bool(re.match(r'^\s*\d+[.)]\s+', line)) + is_list_item = is_bullet or is_ordered + is_heading = bool(re.match(r'^#{1,6}\s+', line)) + + if is_list_item: + new_type = "orderedList" if is_ordered else "bulletList" + if in_list and new_type != list_type: + _flush_list() + if not in_list and mixed_content: + para_text = "\n".join(mixed_content).strip() + if para_text: + doc["content"].append({"type": "paragraph", "content": parse_inline_multiline(para_text)}) + mixed_content = [] + in_list = True + list_type = new_type + if is_ordered: + item_text = re.sub(r'^\s*\d+[.)]\s+', '', line).strip() + else: + item_text = re.sub(r'^\s*[-*]\s+', '', line).strip() + list_items.append(item_text) + elif is_heading: + _flush_list() + if mixed_content: + para_text = "\n".join(mixed_content).strip() + if para_text: + doc["content"].append({"type": "paragraph", "content": parse_inline_multiline(para_text)}) + mixed_content = [] + hm = re.match(r'^(#{1,6})\s+(.+)$', line) + doc["content"].append({ + "type": "heading", + "attrs": {"level": len(hm.group(1))}, + "content": parse_inline(hm.group(2)), + }) + else: + _flush_list() + if line.strip() == "---": + if mixed_content: + para_text = "\n".join(mixed_content).strip() + if para_text: + doc["content"].append({"type": "paragraph", "content": parse_inline_multiline(para_text)}) + mixed_content = [] + doc["content"].append({"type": "rule"}) + else: + mixed_content.append(line) + + _flush_list() + if mixed_content: + para_text = "\n".join(mixed_content).strip() + if para_text: + doc["content"].append({"type": "paragraph", "content": parse_inline_multiline(para_text)}) + + if not doc["content"]: + doc["content"].append({"type": "paragraph", "content": [{"type": "text", "text": text or " "}]}) + + return doc + + +if __name__ == "__main__": + raw = sys.stdin.read(MAX_INPUT_BYTES + 1) + if len(raw) > MAX_INPUT_BYTES: + print(f"ERROR: input exceeds {MAX_INPUT_BYTES} bytes", file=sys.stderr) + sys.exit(1) + adf = text_to_adf(raw) + print(json.dumps({"body": adf}, ensure_ascii=False)) diff --git a/internal/scaffold/fullsend-repo/scripts/pipeline-events.sh b/internal/scaffold/fullsend-repo/scripts/pipeline-events.sh new file mode 100755 index 000000000..5b56a191f --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/pipeline-events.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# pipeline-events.sh — Emit structured pipeline events for full-trace observability. +# +# Source this file in pre/post scripts to record timing and metadata for each +# pipeline phase. Events are appended to a JSONL file that send-trace.py reads +# to build a hierarchical trace spanning the entire workflow — not just the +# Claude sandbox. +# +# When otel-trace-context.sh is also sourced and initialized, events are +# enriched with trace_id, span_id, and parent_span_id fields for proper +# distributed trace correlation. +# +# Usage: +# source "$(dirname "${BASH_SOURCE[0]}")/pipeline-events.sh" +# pe_start "pre-explore" "fetch-issue" +# ... do work ... +# pe_end "pre-explore" "fetch-issue" '{"children": 5, "comments": 3}' +# +# The events file path defaults to /tmp/workspace/pipeline-events.jsonl and +# is also written to $GITHUB_WORKSPACE/output/pipeline-events.jsonl for +# artifact upload. + +PIPELINE_EVENTS_DIR="/tmp/workspace" +PIPELINE_EVENTS_FILE="${PIPELINE_EVENTS_DIR}/pipeline-events.jsonl" +mkdir -p "$PIPELINE_EVENTS_DIR" + +# Auto-source OTEL trace context if available and not already loaded +_PE_SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [[ -z "${_OTEL_INITIALIZED:-}" && -f "${_PE_SCRIPT_DIR}/otel-trace-context.sh" ]]; then + source "${_PE_SCRIPT_DIR}/otel-trace-context.sh" +fi + +_pe_now_ms() { + python3 -c "import time; print(int(time.time() * 1000))" 2>/dev/null \ + || date +%s%3N 2>/dev/null \ + || echo "$(date +%s)000" +} + +_pe_now_iso() { + date -u +"%Y-%m-%dT%H:%M:%S.%3NZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ" +} + +pe_start() { + local phase="$1" step="$2" + local ts_ms; ts_ms=$(_pe_now_ms) + local ts_iso; ts_iso=$(_pe_now_iso) + + # Start an OTEL span if trace context is initialized + if [[ -n "${_OTEL_INITIALIZED:-}" ]]; then + otel_start_span "${phase}:${step}" + fi + + jq -nc \ + --arg phase "$phase" \ + --arg step "$step" \ + --arg event "start" \ + --arg ts "$ts_iso" \ + --argjson ts_ms "$ts_ms" \ + --arg trace_id "${_OTEL_TRACE_ID:-}" \ + '{phase: $phase, step: $step, event: $event, timestamp: $ts, timestamp_ms: $ts_ms} + + (if $trace_id != "" then {trace_id: $trace_id} else {} end)' \ + >> "$PIPELINE_EVENTS_FILE" +} + +pe_end() { + local phase="$1" step="$2" + local metadata="$3" + [[ -z "$metadata" ]] && metadata='{}' + local ts_ms; ts_ms=$(_pe_now_ms) + local ts_iso; ts_iso=$(_pe_now_iso) + + if ! echo "$metadata" | jq empty 2>/dev/null; then + metadata="{}" + fi + + # End the OTEL span if trace context is initialized + if [[ -n "${_OTEL_INITIALIZED:-}" ]]; then + otel_end_span "ok" "$metadata" + fi + + jq -nc \ + --arg phase "$phase" \ + --arg step "$step" \ + --arg event "end" \ + --arg ts "$ts_iso" \ + --argjson ts_ms "$ts_ms" \ + --argjson meta "$metadata" \ + --arg trace_id "${_OTEL_TRACE_ID:-}" \ + '{phase: $phase, step: $step, event: $event, timestamp: $ts, timestamp_ms: $ts_ms, metadata: $meta} + + (if $trace_id != "" then {trace_id: $trace_id} else {} end)' \ + >> "$PIPELINE_EVENTS_FILE" +} + +pe_error() { + local phase="$1" step="$2" error_msg="$3" + local ts_ms; ts_ms=$(_pe_now_ms) + local ts_iso; ts_iso=$(_pe_now_iso) + + # End the OTEL span with error status + if [[ -n "${_OTEL_INITIALIZED:-}" ]]; then + otel_end_span "error" "$(jq -nc --arg err "$error_msg" '{error: $err}')" + fi + + jq -nc \ + --arg phase "$phase" \ + --arg step "$step" \ + --arg event "error" \ + --arg ts "$ts_iso" \ + --argjson ts_ms "$ts_ms" \ + --arg error "$error_msg" \ + --arg trace_id "${_OTEL_TRACE_ID:-}" \ + '{phase: $phase, step: $step, event: $event, timestamp: $ts, timestamp_ms: $ts_ms, error: $error} + + (if $trace_id != "" then {trace_id: $trace_id} else {} end)' \ + >> "$PIPELINE_EVENTS_FILE" +} + +pe_copy_to_output() { + local output_dir="${1:-${GITHUB_WORKSPACE:-$(pwd)}/output}" + if [[ -f "$PIPELINE_EVENTS_FILE" ]]; then + mkdir -p "$output_dir" + cp "$PIPELINE_EVENTS_FILE" "$output_dir/pipeline-events.jsonl" + fi + # Also copy OTEL trace files if they exist + for f in otel-spans.jsonl otel-trace-context.json; do + if [[ -f "${OTEL_SPANS_DIR:-/tmp/workspace}/$f" ]]; then + cp "${OTEL_SPANS_DIR:-/tmp/workspace}/$f" "$output_dir/" + fi + done +} diff --git a/internal/scaffold/fullsend-repo/scripts/pipeline-helpers.sh b/internal/scaffold/fullsend-repo/scripts/pipeline-helpers.sh new file mode 100755 index 000000000..b51b93012 --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/pipeline-helpers.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# pipeline-helpers.sh — Shared helpers for refinement pipeline post-scripts. +# +# Source this file from post-explore.sh, post-refine.sh, post-critique.sh. +# Requires: GH_TOKEN, JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN (Jira path only) + +# Prevent double-sourcing +[[ -n "${_PIPELINE_HELPERS_LOADED:-}" ]] && return 0 +_PIPELINE_HELPERS_LOADED=1 + +SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}" + +add_label() { + local repo="$1" number="$2" label="$3" + gh api "repos/${repo}/issues/${number}/labels" -f "labels[]=${label}" --silent 2>/dev/null || true +} + +remove_label() { + local repo="$1" number="$2" label="$3" + local encoded + encoded=$(printf '%s' "$label" | jq -sRr @uri) + gh api "repos/${repo}/issues/${number}/labels/${encoded}" -X DELETE --silent 2>/dev/null || true +} + +github_comment() { + local repo="$1" number="$2" body="$3" + printf '%s' "$body" | gh issue comment "$number" --repo "$repo" --body-file - +} + +jira_comment() { + local key="$1" body="$2" + local auth + auth=$(printf '%s:%s' "$JIRA_EMAIL" "$JIRA_API_TOKEN" | base64 -w0) + local adf_body + adf_body=$(printf '%s' "$body" | python3 "${SCRIPT_DIR}/markdown-to-adf.py") + curl -sSf -X POST \ + -H "Authorization: Basic $auth" \ + -H "Content-Type: application/json" \ + -d "$adf_body" \ + "https://${JIRA_HOST}/rest/api/3/issue/${key}/comment" +} + +# post_comment dispatches to GitHub or Jira based on USE_GITHUB. +# Callers must set USE_GITHUB, REPO_FULL_NAME, GITHUB_ISSUE_NUMBER, ISSUE_KEY. +post_comment() { + local body="$1" + if ${USE_GITHUB:-false}; then + github_comment "${REPO_FULL_NAME}" "$GITHUB_ISSUE_NUMBER" "$body" + else + jira_comment "$ISSUE_KEY" "$body" + fi +} + +# Determine reply target from environment. Sets USE_GITHUB and GITHUB_ISSUE_NUMBER. +determine_reply_target() { + USE_GITHUB=false + if [[ -n "${GITHUB_ISSUE_NUMBER:-}" && "${GITHUB_ISSUE_NUMBER}" != "" && "${GITHUB_ISSUE_NUMBER}" != "N/A" ]]; then + USE_GITHUB=true + elif [[ "${ISSUE_SOURCE:-}" == "github" ]]; then + USE_GITHUB=true + GITHUB_ISSUE_NUMBER="${ISSUE_KEY}" + fi +} + +# Build a run link from GITHUB_REPOSITORY and GITHUB_RUN_ID. +build_run_link() { + local run_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID:-}" + echo "[Run #${GITHUB_RUN_ID:-manual}](${run_url})" +} + +# Find the last agent-result.json from iteration output directories. +find_agent_result() { + local result_file="" + for dir in iteration-*/output; do + if [[ -f "${dir}/agent-result.json" ]]; then + result_file="${dir}/agent-result.json" + fi + done + if [[ -z "$result_file" ]]; then + echo "ERROR: agent-result.json not found in any iteration output directory" >&2 + return 1 + fi + if ! jq empty "$result_file" 2>/dev/null; then + echo "ERROR: ${result_file} is not valid JSON" >&2 + return 1 + fi + echo "$result_file" +} + +# Read trace context for distributed tracing propagation. +get_traceparent() { + local tp="${TRACEPARENT:-}" + if [[ -z "$tp" && -f "/tmp/workspace/otel-trace-context.json" ]]; then + tp=$(python3 -c "import json; print(json.load(open('/tmp/workspace/otel-trace-context.json'))['traceparent'])" 2>/dev/null || echo "") + fi + echo "$tp" +} diff --git a/internal/scaffold/fullsend-repo/scripts/post-critique-test.sh b/internal/scaffold/fullsend-repo/scripts/post-critique-test.sh new file mode 100755 index 000000000..bdccc94b3 --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/post-critique-test.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash +# post-critique-test.sh — Test post-critique.sh verdict routing and iteration control. +# +# Run from the repo root: bash internal/scaffold/fullsend-repo/scripts/post-critique-test.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +POST_SCRIPT="${SCRIPT_DIR}/post-critique.sh" +FAILURES=0 + +TEST_TMPDIR="$(mktemp -d)" +trap 'rm -rf "${TEST_TMPDIR}"' EXIT + +GH_LOG="${TEST_TMPDIR}/gh-calls.log" +MOCK_BIN="${TEST_TMPDIR}/bin" +mkdir -p "${MOCK_BIN}" + +cat > "${MOCK_BIN}/gh" <<'MOCKEOF' +#!/usr/bin/env bash +echo "gh $*" >> "$GH_LOG" + +case "$*" in + *"issue comment"*) + cat > /dev/null + exit 0 + ;; + *"workflow run"*) + exit 0 + ;; + *"api"*) + exit 0 + ;; +esac +exit 0 +MOCKEOF +chmod +x "${MOCK_BIN}/gh" + +cat > "${MOCK_BIN}/python3" <<'MOCKEOF' +#!/usr/bin/env bash +if [[ "${1:-}" == "-c" ]]; then + if [[ "${2:-}" == *"time.time"* ]]; then + echo "1000000" + exit 0 + fi + echo "" + exit 0 +fi +exec /usr/bin/python3 "$@" +MOCKEOF +chmod +x "${MOCK_BIN}/python3" + +export PATH="${MOCK_BIN}:${PATH}" +export GH_LOG="${GH_LOG}" +export GH_TOKEN="fake-token" +export ISSUE_KEY="42" +export ISSUE_SOURCE="github" +export REPO_FULL_NAME="test-org/test-repo" +export GITHUB_REPOSITORY="test-org/.fullsend" +export GITHUB_RUN_ID="99999" +export GITHUB_ISSUE_NUMBER="42" +export GITHUB_WORKSPACE="${TEST_TMPDIR}" +export REFINE_RUN_ID="88888" + +APPROVED_FIXTURE='{ + "verdict": "approved", + "comment": "Plan looks good.", + "assessment": {"overall": 90}, + "revisions": [] +}' + +REVISE_FIXTURE='{ + "verdict": "revise", + "comment": "Needs adjustments.", + "assessment": {"overall": 55}, + "revisions": [{"type": "revise", "target": "Child 1", "reason": "Missing AC"}] +}' + +NEEDS_INPUT_FIXTURE='{ + "verdict": "needs_input", + "comment": "Cannot evaluate without clarification.", + "assessment": {"overall": 40}, + "question": {"dimension": "scope", "text": "What is the target platform?", "impact": "Determines child decomposition"} +}' + +UNKNOWN_FIXTURE='{ + "verdict": "banana", + "comment": "This should fail.", + "assessment": {"overall": 0} +}' + +# Pre-populate refine result for approved path +mkdir -p /tmp/workspace +echo '{"children": [{"title": "c1"}, {"title": "c2"}]}' > /tmp/workspace/refine-result.json + +run_test() { + local test_name="$1" + local fixture="$2" + local extra_env="$3" + local expect_failure="${4:-false}" + + local run_dir="${TEST_TMPDIR}/run-${test_name}" + mkdir -p "${run_dir}/iteration-1/output" + echo "${fixture}" > "${run_dir}/iteration-1/output/agent-result.json" + + # Clean up workspace state between tests + rm -f /tmp/workspace/critique-history.json + rm -f /tmp/workspace/critique-feedback.json + + : > "${GH_LOG}" + + local exit_code=0 + (cd "${run_dir}" && eval "${extra_env}" bash "${POST_SCRIPT}") > "${TEST_TMPDIR}/stdout-${test_name}.log" 2>&1 || exit_code=$? + + if [[ "${expect_failure}" == "true" ]]; then + if [[ ${exit_code} -eq 0 ]]; then + echo "FAIL: ${test_name} — expected failure but got success" + FAILURES=$((FAILURES + 1)) + return + fi + echo "PASS: ${test_name} (expected failure)" + return + fi + + if [[ ${exit_code} -ne 0 ]]; then + echo "FAIL: ${test_name} — exit code ${exit_code}" + cat "${TEST_TMPDIR}/stdout-${test_name}.log" + FAILURES=$((FAILURES + 1)) + return + fi + + echo "PASS: ${test_name}" +} + +assert_gh_called() { + local test_name="$1" pattern="$2" + if ! grep -qF "${pattern}" "${GH_LOG}"; then + echo "FAIL: ${test_name} — expected gh call matching '${pattern}'" + cat "${GH_LOG}" + FAILURES=$((FAILURES + 1)) + fi +} + +assert_gh_not_called() { + local test_name="$1" pattern="$2" + if grep -qF "${pattern}" "${GH_LOG}"; then + echo "FAIL: ${test_name} — unexpected gh call matching '${pattern}'" + cat "${GH_LOG}" + FAILURES=$((FAILURES + 1)) + fi +} + +assert_stdout_contains() { + local test_name="$1" pattern="$2" + if ! grep -qF "${pattern}" "${TEST_TMPDIR}/stdout-${test_name}.log"; then + echo "FAIL: ${test_name} — expected stdout containing '${pattern}'" + FAILURES=$((FAILURES + 1)) + fi +} + +# --- Tests --- + +# 1. Approved verdict with auto-create disabled (default) — adds label +run_test "approved-no-auto-create" "$APPROVED_FIXTURE" "REVIEW_ROUND=1 MAX_REVIEW_ROUNDS=3 AUTO_CREATE=false" +assert_gh_called "approved-no-auto-create" "issue comment" +assert_gh_called "approved-no-auto-create" "refine-approved" +assert_gh_not_called "approved-no-auto-create" "workflow run" +assert_stdout_contains "approved-no-auto-create" "Post-critique complete" + +# 2. Revise verdict under limit — chains back to refine +run_test "revise-under-limit" "$REVISE_FIXTURE" "REVIEW_ROUND=1 MAX_REVIEW_ROUNDS=3" +assert_gh_called "revise-under-limit" "workflow run refine.yml" +assert_gh_called "revise-under-limit" "review_round=2" +assert_gh_called "revise-under-limit" "issue comment" +assert_stdout_contains "revise-under-limit" "Post-critique complete" + +# 3. Revise at max rounds — escalates to human +run_test "revise-max-rounds" "$REVISE_FIXTURE" "REVIEW_ROUND=3 MAX_REVIEW_ROUNDS=3" +assert_gh_called "revise-max-rounds" "refine-needs-human" +assert_gh_called "revise-max-rounds" "refine-approved" +assert_gh_not_called "revise-max-rounds" "workflow run refine.yml" +assert_stdout_contains "revise-max-rounds" "Post-critique complete" + +# 4. Needs input — posts question and adds label +run_test "needs-input" "$NEEDS_INPUT_FIXTURE" "REVIEW_ROUND=1 MAX_REVIEW_ROUNDS=3" +assert_gh_called "needs-input" "refine-needs-input" +assert_gh_called "needs-input" "issue comment" +assert_gh_not_called "needs-input" "workflow run" +assert_stdout_contains "needs-input" "Post-critique complete" + +# 5. Unknown verdict — should fail +run_test "unknown-verdict" "$UNKNOWN_FIXTURE" "REVIEW_ROUND=1 MAX_REVIEW_ROUNDS=3" "true" + +# 6. Critique history accumulates across rounds +run_test "history-round-1" "$REVISE_FIXTURE" "REVIEW_ROUND=1 MAX_REVIEW_ROUNDS=3" +if [[ -f /tmp/workspace/critique-history.json ]]; then + HISTORY_ROUNDS=$(jq '.rounds | length' /tmp/workspace/critique-history.json) + if [[ "$HISTORY_ROUNDS" == "1" ]]; then + echo "PASS: history-accumulation" + else + echo "FAIL: history-accumulation — expected 1 round in history, got ${HISTORY_ROUNDS}" + FAILURES=$((FAILURES + 1)) + fi +else + echo "FAIL: history-accumulation — critique-history.json not found" + FAILURES=$((FAILURES + 1)) +fi + +if [[ ${FAILURES} -gt 0 ]]; then + echo "" + echo "${FAILURES} test(s) failed." + exit 1 +fi + +echo "" +echo "All post-critique tests passed." diff --git a/internal/scaffold/fullsend-repo/scripts/post-critique.sh b/internal/scaffold/fullsend-repo/scripts/post-critique.sh new file mode 100755 index 000000000..df3f76530 --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/post-critique.sh @@ -0,0 +1,269 @@ +#!/usr/bin/env bash +# post-critique.sh — Process critique agent output. +# +# Reads the critique result and performs one of: +# - verdict=approved + AUTO_CREATE=true: creates child issues immediately +# - verdict=approved + AUTO_CREATE=false: posts approval, adds label for human gate +# - verdict=revise + under iteration limit: posts feedback, chains back to refine +# - verdict=revise + at iteration limit: posts final plan for human decision +# +# Required env vars: +# ISSUE_KEY — Issue identifier (Jira key or GH issue number) +# ISSUE_SOURCE — "jira" or "github" +# GH_TOKEN — GitHub token +# +# GitHub flow env vars: +# GITHUB_ISSUE_NUMBER — GitHub issue number +# REPO_FULL_NAME — owner/repo +# PUSH_TOKEN — Token with write access +# +# Jira flow env vars: +# JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN +# +# Critique flow env vars: +# REVIEW_ROUND — Current review round (default: 1) +# MAX_REVIEW_ROUNDS — Max rounds (default: 3) +# AUTO_CREATE — "true" to auto-create on approval (default: "false") +# REFINE_RUN_ID — Run ID of the refine stage + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/pipeline-events.sh" +source "${SCRIPT_DIR}/pipeline-helpers.sh" + +pe_start "post-critique" "post-critique" + +REVIEW_ROUND="${REVIEW_ROUND:-1}" +MAX_REVIEW_ROUNDS="${MAX_REVIEW_ROUNDS:-3}" +AUTO_CREATE="${AUTO_CREATE:-false}" + +RESULT_FILE=$(find_agent_result) || exit 1 +echo "Reading critique result from: ${RESULT_FILE}" + +VERDICT=$(jq -r '.verdict' "${RESULT_FILE}") +COMMENT=$(jq -r '.comment // ""' "${RESULT_FILE}") +OVERALL_SCORE=$(jq -r '.assessment.overall // 0' "${RESULT_FILE}") +REVISION_COUNT=$(jq '.revisions // [] | length' "${RESULT_FILE}") + +echo "Verdict: ${VERDICT}, Overall score: ${OVERALL_SCORE}, Revisions: ${REVISION_COUNT}, Round: ${REVIEW_ROUND}/${MAX_REVIEW_ROUNDS}" + +determine_reply_target +RUN_LINK=$(build_run_link) + +AGENT_HEADER="🔎 **Critique Agent** · ${RUN_LINK} · Review Round ${REVIEW_ROUND}" + +echo "Reply target: $(if $USE_GITHUB; then echo "GitHub #${GITHUB_ISSUE_NUMBER}"; else echo "Jira ${ISSUE_KEY}"; fi)" + +# --- Update critique history --- +# Accumulate review rounds for the next iteration's context +CRITIQUE_HISTORY_FILE="/tmp/workspace/critique-history.json" +if [[ -f "$CRITIQUE_HISTORY_FILE" ]]; then + UPDATED_HISTORY=$(jq --argjson round "$REVIEW_ROUND" \ + --arg verdict "$VERDICT" \ + --argjson score "$OVERALL_SCORE" \ + --argjson revisions "$(jq '.revisions // []' "$RESULT_FILE")" \ + '.rounds += [{"round": $round, "verdict": $verdict, "overall_score": $score, "revisions": $revisions}]' \ + "$CRITIQUE_HISTORY_FILE") + echo "$UPDATED_HISTORY" > "$CRITIQUE_HISTORY_FILE" +else + jq -n --argjson round "$REVIEW_ROUND" \ + --arg verdict "$VERDICT" \ + --argjson score "$OVERALL_SCORE" \ + --argjson revisions "$(jq '.revisions // []' "$RESULT_FILE")" \ + '{rounds: [{"round": $round, "verdict": $verdict, "overall_score": $score, "revisions": $revisions}]}' \ + > "$CRITIQUE_HISTORY_FILE" +fi + +# --- Process based on verdict --- + +if [[ "${VERDICT}" == "approved" ]]; then + pe_start "post-critique" "handle-approval" + echo "::notice::Critique approved the refinement plan (round ${REVIEW_ROUND})" + + FULL_COMMENT="${AGENT_HEADER} + +**Verdict: ✅ Approved** (score: ${OVERALL_SCORE}/100) + +${COMMENT}" + + if [[ "${AUTO_CREATE}" == "true" ]]; then + echo "Auto-create enabled — creating child issues..." + + # Post the approval comment first + post_comment "$FULL_COMMENT" + + # Find the refine result to create children from + REFINE_RESULT_FILE="/tmp/workspace/refine-result.json" + if [[ ! -f "$REFINE_RESULT_FILE" ]]; then + echo "::error::Refine result not found at ${REFINE_RESULT_FILE}" + exit 1 + fi + + # Delegate to create-children.sh + export RESULT_FILE="$REFINE_RESULT_FILE" + bash "${SCRIPT_DIR}/create-children.sh" + + CHILD_SUMMARY="Created ${CREATED_CHILD_COUNT:-0} child issue(s): ${CREATED_CHILD_KEYS:-none}" + echo "::notice::${CHILD_SUMMARY}" + + CREATION_COMMENT="📦 **Issue Creator** · Run #${GITHUB_RUN_ID:-manual} + +**Child issues created** after critique approval. + +${CHILD_SUMMARY}" + post_comment "$CREATION_COMMENT" + + else + echo "Auto-create disabled — posting approval for human review" + + PLAN_CHILD_COUNT=$(jq '.children | length' "/tmp/workspace/refine-result.json" 2>/dev/null || echo "0") + + APPROVAL_COMMENT="${FULL_COMMENT} + +--- +**Ready for human approval.** The plan proposes ${PLAN_CHILD_COUNT} child issue(s). + +To create the child issues, comment \`/fs-create\`. +To request further changes, reply with your feedback." + + post_comment "$APPROVAL_COMMENT" + + if $USE_GITHUB; then + add_label "${REPO_FULL_NAME}" "$GITHUB_ISSUE_NUMBER" "refine-approved" + fi + fi + + pe_end "post-critique" "handle-approval" "$(jq -nc --arg auto_create "$AUTO_CREATE" --argjson score "$OVERALL_SCORE" '{auto_create:$auto_create, overall_score:$score}')" + +elif [[ "${VERDICT}" == "revise" ]]; then + NEXT_ROUND=$((REVIEW_ROUND + 1)) + + if [[ $NEXT_ROUND -gt $MAX_REVIEW_ROUNDS ]]; then + pe_start "post-critique" "handle-max-iterations" + echo "::warning::Max review rounds (${MAX_REVIEW_ROUNDS}) reached — escalating to human" + + PLAN_CHILD_COUNT=$(jq '.children | length' "/tmp/workspace/refine-result.json" 2>/dev/null || echo "0") + + ESCALATION_COMMENT="${AGENT_HEADER} + +**Verdict: ⚠️ Max review rounds reached** (${MAX_REVIEW_ROUNDS} rounds, score: ${OVERALL_SCORE}/100) + +${COMMENT} + +--- +**Human decision needed.** The critique agent still has concerns after ${MAX_REVIEW_ROUNDS} rounds of review. + +The current plan proposes ${PLAN_CHILD_COUNT} child issue(s). Options: +- Reply \`/fs-create\` to create the issues as-is +- Reply \`/fs-refine\` to restart the refinement process +- Reply with specific guidance for the refine agent" + + post_comment "$ESCALATION_COMMENT" + + if $USE_GITHUB; then + add_label "${REPO_FULL_NAME}" "$GITHUB_ISSUE_NUMBER" "refine-needs-human" + add_label "${REPO_FULL_NAME}" "$GITHUB_ISSUE_NUMBER" "refine-approved" + fi + + # Mark critique history as approved so create-children.yml can verify + if [[ -f "$CRITIQUE_HISTORY_FILE" ]]; then + UPDATED=$(jq '.rounds[-1].verdict = "approved" | .rounds[-1].escalated = true' "$CRITIQUE_HISTORY_FILE") + echo "$UPDATED" > "$CRITIQUE_HISTORY_FILE" + fi + + pe_end "post-critique" "handle-max-iterations" "$(jq -nc --argjson round "$REVIEW_ROUND" --argjson score "$OVERALL_SCORE" '{round:$round, score:$score}')" + + else + pe_start "post-critique" "handle-revision" + echo "::notice::Critique requests revisions — chaining back to refine (round ${NEXT_ROUND})" + + REVISION_COMMENT="${AGENT_HEADER} + +**Verdict: 🔄 Revisions requested** (score: ${OVERALL_SCORE}/100, ${REVISION_COUNT} revision(s)) + +${COMMENT}" + + post_comment "$REVISION_COMMENT" + + # Save critique result for refine to read + cp "$RESULT_FILE" "/tmp/workspace/critique-feedback.json" + + # Chain back to refine with review context + WORKFLOW_REPO="${GITHUB_REPOSITORY}" + TARGET_REPO="${REPO_FULL_NAME:-}" + THIS_RUN_ID="${GITHUB_RUN_ID:-}" + + if [[ -n "$THIS_RUN_ID" ]]; then + CURRENT_TRACEPARENT=$(get_traceparent) + + CHAIN_ARGS=( + --repo "$WORKFLOW_REPO" + -f issue_key="${ISSUE_KEY}" + -f issue_source="${ISSUE_SOURCE}" + -f critique_run_id="${THIS_RUN_ID}" + -f review_round="${NEXT_ROUND}" + -f max_review_rounds="${MAX_REVIEW_ROUNDS}" + -f auto_create="${AUTO_CREATE}" + ) + + # Only pass repo_full_name if a target repo was specified + if [[ -n "$TARGET_REPO" ]]; then + CHAIN_ARGS+=(-f repo_full_name="${TARGET_REPO}") + fi + + if [[ -n "$CURRENT_TRACEPARENT" ]]; then + CHAIN_ARGS+=(-f parent_traceparent="${CURRENT_TRACEPARENT}") + fi + + if [[ -n "${GITHUB_ISSUE_NUMBER:-}" && "${GITHUB_ISSUE_NUMBER}" != "N/A" ]]; then + CHAIN_ARGS+=(-f github_issue_number="${GITHUB_ISSUE_NUMBER}") + fi + + gh workflow run refine.yml "${CHAIN_ARGS[@]}" \ + 2>/dev/null || echo "::warning::Failed to chain refine workflow — trigger manually" + else + echo "::warning::GITHUB_RUN_ID not available — refine must be triggered manually" + fi + + pe_end "post-critique" "handle-revision" "$(jq -nc --argjson next_round "$NEXT_ROUND" --argjson revisions "$REVISION_COUNT" '{next_round:$next_round, revision_count:$revisions}')" + fi + +elif [[ "${VERDICT}" == "needs_input" ]]; then + pe_start "post-critique" "handle-needs-input" + echo "::notice::Critique needs human input — posting question" + + QUESTION_DIM=$(jq -r '.question.dimension // "unknown"' "${RESULT_FILE}") + QUESTION_TEXT=$(jq -r '.question.text // ""' "${RESULT_FILE}") + QUESTION_IMPACT=$(jq -r '.question.impact // ""' "${RESULT_FILE}") + + QUESTION_COMMENT="${AGENT_HEADER} + +**Verdict: ❓ Needs Human Input** (score: ${OVERALL_SCORE}/100) + +${COMMENT} + +--- +**Question** (${QUESTION_DIM}): ${QUESTION_TEXT} + +**Why this matters**: ${QUESTION_IMPACT} + +Reply with your answer, then comment \`/fs-refine\` to restart the pipeline with the new context." + + post_comment "$QUESTION_COMMENT" + + if $USE_GITHUB; then + add_label "${REPO_FULL_NAME}" "$GITHUB_ISSUE_NUMBER" "refine-needs-input" + fi + + pe_end "post-critique" "handle-needs-input" "$(jq -nc --arg dim "$QUESTION_DIM" --argjson score "$OVERALL_SCORE" '{dimension:$dim, score:$score}')" + +else + echo "ERROR: Unknown verdict '${VERDICT}'" + exit 1 +fi + +pe_end "post-critique" "post-critique" "$(jq -nc --arg verdict "$VERDICT" --argjson score "$OVERALL_SCORE" --argjson round "$REVIEW_ROUND" '{verdict:$verdict, score:$score, round:$round}')" +pe_copy_to_output + +echo "Post-critique complete." diff --git a/internal/scaffold/fullsend-repo/scripts/post-explore-test.sh b/internal/scaffold/fullsend-repo/scripts/post-explore-test.sh new file mode 100755 index 000000000..efce79e0e --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/post-explore-test.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# post-explore-test.sh — Test post-explore.sh with fixture JSON and mock gh. +# +# Run from the repo root: bash internal/scaffold/fullsend-repo/scripts/post-explore-test.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +POST_SCRIPT="${SCRIPT_DIR}/post-explore.sh" +FAILURES=0 + +TEST_TMPDIR="$(mktemp -d)" +trap 'rm -rf "${TEST_TMPDIR}"' EXIT + +GH_LOG="${TEST_TMPDIR}/gh-calls.log" +MOCK_BIN="${TEST_TMPDIR}/bin" +mkdir -p "${MOCK_BIN}" + +cat > "${MOCK_BIN}/gh" <<'MOCKEOF' +#!/usr/bin/env bash +echo "gh $*" >> "$GH_LOG" + +case "$*" in + *"workflow run"*) + exit 0 + ;; + *"issue comment"*) + cat > /dev/null # consume stdin + exit 0 + ;; + *"api"*) + exit 0 + ;; +esac +exit 0 +MOCKEOF +chmod +x "${MOCK_BIN}/gh" + +cat > "${MOCK_BIN}/python3" <<'MOCKEOF' +#!/usr/bin/env bash +if [[ "${1:-}" == "-c" ]]; then + # Handle _pe_now_ms calls from pipeline-events.sh + if [[ "${2:-}" == *"time.time"* ]]; then + echo "1000000" + exit 0 + fi + # Handle get_traceparent json.load + echo "" + exit 0 +fi +exec /usr/bin/python3 "$@" +MOCKEOF +chmod +x "${MOCK_BIN}/python3" + +export PATH="${MOCK_BIN}:${PATH}" +export GH_LOG="${GH_LOG}" +export GH_TOKEN="fake-token" +export ISSUE_KEY="42" +export ISSUE_SOURCE="github" +export REPO_FULL_NAME="test-org/test-repo" +export GITHUB_REPOSITORY="test-org/.fullsend" +export GITHUB_RUN_ID="12345" +export GITHUB_ISSUE_NUMBER="42" +export GITHUB_WORKSPACE="${TEST_TMPDIR}" + +EXPLORE_FIXTURE='{ + "confidence": {"overall": 85}, + "gaps": ["gap1"], + "related_work": [{"title": "PR #1"}, {"title": "PR #2"}], + "summary": "Test exploration summary.", + "technical_landscape": {"languages": ["Go"]} +}' + +run_test() { + local test_name="$1" + local fixture="${2:-$EXPLORE_FIXTURE}" + local expect_failure="${3:-false}" + + local run_dir="${TEST_TMPDIR}/run-${test_name}" + mkdir -p "${run_dir}/iteration-1/output" + echo "${fixture}" > "${run_dir}/iteration-1/output/agent-result.json" + + : > "${GH_LOG}" + + local exit_code=0 + (cd "${run_dir}" && bash "${POST_SCRIPT}") > "${TEST_TMPDIR}/stdout-${test_name}.log" 2>&1 || exit_code=$? + + if [[ "${expect_failure}" == "true" ]]; then + if [[ ${exit_code} -eq 0 ]]; then + echo "FAIL: ${test_name} — expected failure but got success" + FAILURES=$((FAILURES + 1)) + return + fi + echo "PASS: ${test_name} (expected failure)" + return + fi + + if [[ ${exit_code} -ne 0 ]]; then + echo "FAIL: ${test_name} — exit code ${exit_code}" + cat "${TEST_TMPDIR}/stdout-${test_name}.log" + FAILURES=$((FAILURES + 1)) + return + fi + + echo "PASS: ${test_name}" +} + +assert_gh_called() { + local test_name="$1" pattern="$2" + if ! grep -qF "${pattern}" "${GH_LOG}"; then + echo "FAIL: ${test_name} — expected gh call matching '${pattern}'" + cat "${GH_LOG}" + FAILURES=$((FAILURES + 1)) + fi +} + +assert_file_exists() { + local test_name="$1" path="$2" + if [[ ! -f "${path}" ]]; then + echo "FAIL: ${test_name} — expected file at ${path}" + FAILURES=$((FAILURES + 1)) + fi +} + +# --- Tests --- + +# Happy path: exploration completes and chains refine +run_test "happy-path" +assert_gh_called "happy-path" "workflow run refine.yml" +assert_gh_called "happy-path" "issue_key=42" +assert_gh_called "happy-path" "explore_run_id=12345" +assert_file_exists "happy-path" "${TEST_TMPDIR}/run-happy-path/../../workspace/exploration_context.json" && true +# Check /tmp/workspace/ was created (the script writes there) +if [[ -f "/tmp/workspace/exploration_context.json" ]]; then + echo "PASS: happy-path exploration_context.json saved" +else + echo "INFO: happy-path — /tmp/workspace not checked (may require cleanup)" +fi + +# Auto-create propagation +export AUTO_CREATE="true" +run_test "auto-create-propagation" +assert_gh_called "auto-create-propagation" "auto_create=true" +unset AUTO_CREATE + +# Missing agent result — run from empty dir with no iteration-*/output/ +test_name="missing-result" +run_dir="${TEST_TMPDIR}/run-${test_name}" +mkdir -p "${run_dir}" +: > "${GH_LOG}" +exit_code=0 +(cd "${run_dir}" && bash "${POST_SCRIPT}") > "${TEST_TMPDIR}/stdout-${test_name}.log" 2>&1 || exit_code=$? +if [[ ${exit_code} -ne 0 ]]; then + echo "PASS: ${test_name} (expected failure)" +else + echo "FAIL: ${test_name} — expected failure but got success" + FAILURES=$((FAILURES + 1)) +fi + +# Invalid JSON result +run_test "invalid-json" "not valid json" "true" + +if [[ ${FAILURES} -gt 0 ]]; then + echo "" + echo "${FAILURES} test(s) failed." + exit 1 +fi + +echo "" +echo "All post-explore tests passed." diff --git a/internal/scaffold/fullsend-repo/scripts/post-explore.sh b/internal/scaffold/fullsend-repo/scripts/post-explore.sh new file mode 100755 index 000000000..e6681e2bd --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/post-explore.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# post-explore.sh — Store exploration results and chain the refine stage. +# +# The explore agent writes its result to agent-result.json. This script +# validates the output, stores it for artifact upload, and triggers the +# refine workflow with this run's ID for artifact correlation. +# +# Required env vars: +# ISSUE_KEY — Issue identifier +# ISSUE_SOURCE — "jira" or "github" +# REPO_FULL_NAME — owner/repo +# GH_TOKEN — GitHub token + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/pipeline-events.sh" +source "${SCRIPT_DIR}/pipeline-helpers.sh" + +pe_start "post-explore" "post-explore" + +RESULT_FILE=$(find_agent_result) || exit 1 +echo "Reading exploration result from: ${RESULT_FILE}" + +pe_start "post-explore" "validate-result" + +OVERALL_CONFIDENCE=$(jq -r '.confidence.overall // 0' "${RESULT_FILE}") +GAP_COUNT=$(jq '.gaps // [] | length' "${RESULT_FILE}") +RELATED_COUNT=$(jq '.related_work | length' "${RESULT_FILE}") + +pe_end "post-explore" "validate-result" "$(jq -nc --argjson conf "$OVERALL_CONFIDENCE" --argjson gaps "$GAP_COUNT" --argjson related "$RELATED_COUNT" '{confidence:$conf, gap_count:$gaps, related_work_count:$related}')" + +echo "::notice::Exploration complete: confidence=${OVERALL_CONFIDENCE}, gaps=${GAP_COUNT}, related_work=${RELATED_COUNT}" + +# Copy to a well-known location for artifact upload +WORKSPACE="/tmp/workspace" +mkdir -p "$WORKSPACE" +cp "${RESULT_FILE}" "${WORKSPACE}/exploration_context.json" + +echo "Exploration context saved to ${WORKSPACE}/exploration_context.json" + +# Chain the refine stage with this run's ID for artifact correlation. +# GITHUB_RUN_ID is set by GitHub Actions automatically. +WORKFLOW_REPO="${GITHUB_REPOSITORY}" +TARGET_REPO="${REPO_FULL_NAME:-}" +THIS_RUN_ID="${GITHUB_RUN_ID:-}" + +if [[ -n "$THIS_RUN_ID" ]]; then + pe_start "post-explore" "chain-refine" + echo "Chaining refine stage with explore run ID: ${THIS_RUN_ID}" + + CURRENT_TRACEPARENT=$(get_traceparent) + + CHAIN_ARGS=( + --repo "$WORKFLOW_REPO" + -f issue_key="${ISSUE_KEY}" + -f issue_source="${ISSUE_SOURCE}" + -f explore_run_id="${THIS_RUN_ID}" + ) + + # Only pass repo_full_name if a target repo was specified (not the config repo) + if [[ -n "$TARGET_REPO" ]]; then + CHAIN_ARGS+=(-f repo_full_name="${TARGET_REPO}") + echo "Propagating target repo: ${TARGET_REPO}" + fi + + # Propagate trace context for distributed tracing + if [[ -n "$CURRENT_TRACEPARENT" ]]; then + CHAIN_ARGS+=(-f parent_traceparent="${CURRENT_TRACEPARENT}") + echo "Propagating trace context: ${CURRENT_TRACEPARENT}" + fi + + # Pass through GitHub issue number for reply-back (GitHub flow) + if [[ -n "${GITHUB_ISSUE_NUMBER:-}" && "${GITHUB_ISSUE_NUMBER}" != "N/A" ]]; then + CHAIN_ARGS+=(-f github_issue_number="${GITHUB_ISSUE_NUMBER}") + fi + + # Pass through auto-create preference to refine → critique chain + if [[ "${AUTO_CREATE:-false}" == "true" ]]; then + CHAIN_ARGS+=(-f auto_create="true") + fi + + # Pass through confidence threshold if explicitly set + if [[ -n "${REFINE_CONFIDENCE_THRESHOLD:-}" ]]; then + CHAIN_ARGS+=(-f confidence_threshold="${REFINE_CONFIDENCE_THRESHOLD}") + fi + + gh workflow run refine.yml "${CHAIN_ARGS[@]}" \ + 2>/dev/null || echo "::warning::Failed to chain refine workflow — trigger manually" + pe_end "post-explore" "chain-refine" "$(jq -nc --arg run_id "$THIS_RUN_ID" --arg traceparent "$CURRENT_TRACEPARENT" '{explore_run_id:$run_id, traceparent:$traceparent}')" +else + echo "::warning::GITHUB_RUN_ID not available — refine must be triggered manually" +fi + +# --- Post exploration summary with agent identity --- +EXPLORE_SUMMARY=$(jq -r '.summary // "Exploration complete."' "${RESULT_FILE}") + +RUN_LINK=$(build_run_link) + +EXPLORE_COMMENT="🔍 **Explore Agent** · ${RUN_LINK} + +**Status: ✅ Exploration Complete** (confidence: ${OVERALL_CONFIDENCE}/100, gaps: ${GAP_COUNT}, related work: ${RELATED_COUNT}) + +${EXPLORE_SUMMARY} + +--- +*Chaining to the Refine Agent for decomposition.*" + +determine_reply_target +post_comment "$EXPLORE_COMMENT" 2>/dev/null || true + +if $USE_GITHUB; then + EVAL_META=$(jq -nc \ + --arg run_id "${GITHUB_RUN_ID:-manual}" \ + --arg agent "explore" \ + --arg issue_key "${ISSUE_KEY}" \ + --arg issue_source "${ISSUE_SOURCE:-unknown}" \ + --arg status "complete" \ + --argjson confidence "$OVERALL_CONFIDENCE" \ + --argjson dimensions "$(jq '.confidence // {}' "${RESULT_FILE}")" \ + --argjson child_count 0 \ + '{run_id:$run_id, agent:$agent, issue_key:$issue_key, issue_source:$issue_source, status:$status, confidence:$confidence, dimensions:$dimensions, child_count:$child_count}') + + EVAL_PROMPT="--- +**Eval this run:** React with :+1: or :-1: on this comment, or reply \`/eval yes\` or \`/eval no \"reason\"\`. +" + + github_comment "${REPO_FULL_NAME:-${GITHUB_REPOSITORY}}" "$GITHUB_ISSUE_NUMBER" "$EVAL_PROMPT" 2>/dev/null || true +fi + +pe_end "post-explore" "post-explore" "$(jq -nc --argjson conf "$OVERALL_CONFIDENCE" --argjson gaps "$GAP_COUNT" '{confidence:$conf, gaps:$gaps}')" +pe_copy_to_output + +echo "Post-explore complete." diff --git a/internal/scaffold/fullsend-repo/scripts/post-refine-test.sh b/internal/scaffold/fullsend-repo/scripts/post-refine-test.sh new file mode 100755 index 000000000..af997485e --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/post-refine-test.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +# post-refine-test.sh — Test post-refine.sh with fixture JSON and mock gh. +# +# Run from the repo root: bash internal/scaffold/fullsend-repo/scripts/post-refine-test.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +POST_SCRIPT="${SCRIPT_DIR}/post-refine.sh" +FAILURES=0 + +TEST_TMPDIR="$(mktemp -d)" +trap 'rm -rf "${TEST_TMPDIR}"' EXIT + +GH_LOG="${TEST_TMPDIR}/gh-calls.log" +MOCK_BIN="${TEST_TMPDIR}/bin" +mkdir -p "${MOCK_BIN}" + +cat > "${MOCK_BIN}/gh" <<'MOCKEOF' +#!/usr/bin/env bash +echo "gh $*" >> "$GH_LOG" + +case "$*" in + *"issue comment"*) + cat > /dev/null + exit 0 + ;; + *"workflow run"*) + exit 0 + ;; + *"api"*) + exit 0 + ;; +esac +exit 0 +MOCKEOF +chmod +x "${MOCK_BIN}/gh" + +cat > "${MOCK_BIN}/python3" <<'MOCKEOF' +#!/usr/bin/env bash +if [[ "${1:-}" == "-c" ]]; then + if [[ "${2:-}" == *"time.time"* ]]; then + echo "1000000" + exit 0 + fi + echo "" + exit 0 +fi +exec /usr/bin/python3 "$@" +MOCKEOF +chmod +x "${MOCK_BIN}/python3" + +export PATH="${MOCK_BIN}:${PATH}" +export GH_LOG="${GH_LOG}" +export GH_TOKEN="fake-token" +export ISSUE_KEY="42" +export ISSUE_SOURCE="github" +export REPO_FULL_NAME="test-org/test-repo" +export GITHUB_REPOSITORY="test-org/.fullsend" +export GITHUB_RUN_ID="12345" +export GITHUB_ISSUE_NUMBER="42" +export GITHUB_WORKSPACE="${TEST_TMPDIR}" + +REFINE_FIXTURE='{ + "status": "complete", + "confidence": {"overall": 78}, + "comment": "Proposed decomposition plan.", + "proposed_description": "Enhanced feature description.", + "children": [ + {"title": "Child 1", "type": "story", "description": "First child", "acceptance_criteria": ["AC1"]}, + {"title": "Child 2", "type": "task", "description": "Second child", "acceptance_criteria": ["AC2"]} + ], + "open_questions": [{"dimension": "scope", "question": "Is this in scope?", "impact": "High"}] +}' + +run_test() { + local test_name="$1" + local fixture="${2:-$REFINE_FIXTURE}" + local expect_failure="${3:-false}" + + local run_dir="${TEST_TMPDIR}/run-${test_name}" + mkdir -p "${run_dir}/iteration-1/output" + echo "${fixture}" > "${run_dir}/iteration-1/output/agent-result.json" + + : > "${GH_LOG}" + + local exit_code=0 + (cd "${run_dir}" && bash "${POST_SCRIPT}") > "${TEST_TMPDIR}/stdout-${test_name}.log" 2>&1 || exit_code=$? + + if [[ "${expect_failure}" == "true" ]]; then + if [[ ${exit_code} -eq 0 ]]; then + echo "FAIL: ${test_name} — expected failure but got success" + FAILURES=$((FAILURES + 1)) + return + fi + echo "PASS: ${test_name} (expected failure)" + return + fi + + if [[ ${exit_code} -ne 0 ]]; then + echo "FAIL: ${test_name} — exit code ${exit_code}" + cat "${TEST_TMPDIR}/stdout-${test_name}.log" + FAILURES=$((FAILURES + 1)) + return + fi + + echo "PASS: ${test_name}" +} + +assert_gh_called() { + local test_name="$1" pattern="$2" + if ! grep -qF "${pattern}" "${GH_LOG}"; then + echo "FAIL: ${test_name} — expected gh call matching '${pattern}'" + cat "${GH_LOG}" + FAILURES=$((FAILURES + 1)) + fi +} + +assert_stdout_contains() { + local test_name="$1" pattern="$2" + if ! grep -qF "${pattern}" "${TEST_TMPDIR}/stdout-${test_name}.log"; then + echo "FAIL: ${test_name} — expected stdout containing '${pattern}'" + FAILURES=$((FAILURES + 1)) + fi +} + +# --- Tests --- + +# Happy path: refine completes and chains critique +run_test "happy-path" +assert_gh_called "happy-path" "workflow run critique.yml" +assert_gh_called "happy-path" "issue_key=42" +assert_gh_called "happy-path" "refine_run_id=12345" +assert_gh_called "happy-path" "review_round=1" +assert_gh_called "happy-path" "issue comment" +assert_stdout_contains "happy-path" "Post-refine complete" + +# Revision round propagation +export REVIEW_ROUND="2" +run_test "revision-round" +assert_gh_called "revision-round" "review_round=2" +assert_stdout_contains "revision-round" "Post-refine complete" +unset REVIEW_ROUND + +# Auto-create pass-through +export AUTO_CREATE="true" +run_test "auto-create-passthrough" +assert_gh_called "auto-create-passthrough" "auto_create=true" +unset AUTO_CREATE + +# Missing agent result — run from empty dir with no iteration-*/output/ +test_name="missing-result" +run_dir="${TEST_TMPDIR}/run-${test_name}" +mkdir -p "${run_dir}" +: > "${GH_LOG}" +exit_code=0 +(cd "${run_dir}" && bash "${POST_SCRIPT}") > "${TEST_TMPDIR}/stdout-${test_name}.log" 2>&1 || exit_code=$? +if [[ ${exit_code} -ne 0 ]]; then + echo "PASS: ${test_name} (expected failure)" +else + echo "FAIL: ${test_name} — expected failure but got success" + FAILURES=$((FAILURES + 1)) +fi + +if [[ ${FAILURES} -gt 0 ]]; then + echo "" + echo "${FAILURES} test(s) failed." + exit 1 +fi + +echo "" +echo "All post-refine tests passed." diff --git a/internal/scaffold/fullsend-repo/scripts/post-refine.sh b/internal/scaffold/fullsend-repo/scripts/post-refine.sh new file mode 100755 index 000000000..800366d0b --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/post-refine.sh @@ -0,0 +1,196 @@ +#!/usr/bin/env bash +# post-refine.sh — Process refine agent output and chain to critique. +# +# The refine agent ALWAYS produces a plan (status=complete). This script +# posts a summary comment and ALWAYS chains to the critique agent regardless +# of confidence. The confidence score is passed to critique as context — +# critique decides whether to approve, request revisions, or escalate. +# +# Issue creation is handled downstream by the critique agent's approval flow, +# NOT by this script. See post-critique.sh and create-children.sh. +# +# Routing: results go back to the same system that owns the work item. +# - GitHub flow: GITHUB_ISSUE_NUMBER is set → post to GitHub issue +# - Jira flow: GITHUB_ISSUE_NUMBER is empty → post to Jira +# +# Required env vars: +# ISSUE_KEY — Issue identifier (Jira key or GH issue number) +# ISSUE_SOURCE — "jira" or "github" +# GH_TOKEN — GitHub token +# +# GitHub flow env vars: +# GITHUB_ISSUE_NUMBER — GitHub issue number to post results to +# REPO_FULL_NAME — owner/repo +# PUSH_TOKEN — Token with write access +# +# Jira flow env vars: +# JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN +# +# Critique flow env vars (passed through from critique → refine loop): +# REVIEW_ROUND — Current review round (default: 1) +# MAX_REVIEW_ROUNDS — Max rounds (default: 3) +# AUTO_CREATE — "true" to auto-create on approval (default: "false") + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/pipeline-events.sh" +source "${SCRIPT_DIR}/pipeline-helpers.sh" + +pe_start "post-refine" "post-refine" + +RESULT_FILE=$(find_agent_result) || exit 1 +echo "Reading refine result from: ${RESULT_FILE}" + +STATUS=$(jq -r '.status' "${RESULT_FILE}") +COMMENT=$(jq -r '.comment // ""' "${RESULT_FILE}") +CONFIDENCE=$(jq -r '.confidence.overall // 0' "${RESULT_FILE}") + +echo "Status: ${STATUS}, Confidence: ${CONFIDENCE}" + +determine_reply_target +echo "Reply target: $(if $USE_GITHUB; then echo "GitHub #${GITHUB_ISSUE_NUMBER}"; else echo "Jira ${ISSUE_KEY}"; fi)" + +REVIEW_ROUND="${REVIEW_ROUND:-1}" +MAX_REVIEW_ROUNDS="${MAX_REVIEW_ROUNDS:-3}" +AUTO_CREATE="${AUTO_CREATE:-false}" +IS_REVISION=$([[ "$REVIEW_ROUND" -gt 1 ]] && echo "true" || echo "false") + +RUN_LINK=$(build_run_link) + +AGENT_HEADER="📋 **Refine Agent** · ${RUN_LINK}" +if [[ "$IS_REVISION" == "true" ]]; then + AGENT_HEADER="${AGENT_HEADER} · Iteration ${REVIEW_ROUND} (revised)" +fi + +# --- Post plan and always chain to critique --- + +CONFIDENCE_INT=$(printf '%.0f' "$CONFIDENCE" 2>/dev/null || echo "0") + +pe_start "post-refine" "post-plan" +echo "::notice::Refine complete (confidence ${CONFIDENCE_INT}/100) — posting proposed plan and chaining critique" + +if $USE_GITHUB; then + remove_label "${REPO_FULL_NAME}" "$GITHUB_ISSUE_NUMBER" "refine-needs-input" + remove_label "${REPO_FULL_NAME}" "$GITHUB_ISSUE_NUMBER" "human-refinement" +fi + +CHILD_COUNT=$(jq '.children | length' "${RESULT_FILE}" 2>/dev/null || echo "0") +OPEN_QUESTION_COUNT=$(jq '.open_questions | length' "${RESULT_FILE}" 2>/dev/null || echo "0") + +EPIC_COUNT=$(jq '[.children[]? | select(.type == "epic")] | length' "${RESULT_FILE}" 2>/dev/null || echo "0") +STORY_COUNT=$(jq '[.children[]? | select(.type == "story")] | length' "${RESULT_FILE}" 2>/dev/null || echo "0") +TASK_COUNT=$(jq '[.children[]? | select(.type == "task")] | length' "${RESULT_FILE}" 2>/dev/null || echo "0") + +PLAN_SUMMARY="Proposed: ${CHILD_COUNT} work items" +PLAN_PARTS=() +[[ "$EPIC_COUNT" -gt 0 ]] && PLAN_PARTS+=("${EPIC_COUNT} epics") +[[ "$STORY_COUNT" -gt 0 ]] && PLAN_PARTS+=("${STORY_COUNT} stories") +[[ "$TASK_COUNT" -gt 0 ]] && PLAN_PARTS+=("${TASK_COUNT} tasks") +if [[ ${#PLAN_PARTS[@]} -gt 0 ]]; then + PLAN_SUMMARY="${PLAN_SUMMARY} ($(IFS=', '; echo "${PLAN_PARTS[*]}"))" +fi + +if [[ "$OPEN_QUESTION_COUNT" -gt 0 ]]; then + PLAN_SUMMARY="${PLAN_SUMMARY} · ${OPEN_QUESTION_COUNT} open question(s)" +fi + +WORKFLOW_REPO="${GITHUB_REPOSITORY:-${REPO_FULL_NAME}}" +ARTIFACT_URL="https://github.com/${WORKFLOW_REPO}/actions/runs/${GITHUB_RUN_ID:-}" + +QUESTIONS_SECTION="" +if [[ "$OPEN_QUESTION_COUNT" -gt 0 ]]; then + QUESTIONS_LIST=$(jq -r '.open_questions[]? | if type == "object" then "- **\(.dimension // "general")**: \(.question // .text // .description // tostring)\n *Impact*: \(.impact // "Unknown")" else "- \(tostring)" end' "${RESULT_FILE}" 2>/dev/null || true) + if [[ -n "$QUESTIONS_LIST" ]]; then + QUESTIONS_SECTION=" +--- + +## Open Questions + +${OPEN_QUESTION_COUNT} question(s) that may affect plan accuracy — reply with answers, then comment \`/fs-refine\` to re-run. + +${QUESTIONS_LIST}" + fi +fi + +PLAN_COMMENT="${AGENT_HEADER} + +**Refinement Plan** (confidence: ${CONFIDENCE}/100) + +${PLAN_SUMMARY} + +${COMMENT} + +📎 [**Full plan details** (all epics, stories, tasks, acceptance criteria)](${ARTIFACT_URL}) — download the \`fullsend-refine\` artifact for the complete \`refine-result.json\`. +${QUESTIONS_SECTION} + +--- +*This plan will be reviewed by the Critique Agent before any issues are created.*" + +post_comment "$PLAN_COMMENT" + +# Post proposed description as a separate comment (standalone document) +PROPOSED_DESC=$(jq -r '.proposed_description // ""' "${RESULT_FILE}") +if [[ -n "$PROPOSED_DESC" && "$PROPOSED_DESC" != "null" ]]; then + DESC_COMMENT="📝 **Refine Agent** · Proposed Feature Description + +The following is a proposed enhanced description for this feature based on exploration research and decomposition analysis. If the plan is approved, this description can replace the current one. + +--- + +${PROPOSED_DESC}" + + post_comment "$DESC_COMMENT" +fi + +# Save the refine result for critique to pick up via artifact +cp "${RESULT_FILE}" "/tmp/workspace/refine-result.json" + +# Chain to the critique agent +WORKFLOW_REPO="${GITHUB_REPOSITORY}" +TARGET_REPO="${REPO_FULL_NAME:-}" +THIS_RUN_ID="${GITHUB_RUN_ID:-}" + +if [[ -n "$THIS_RUN_ID" ]]; then + pe_start "post-refine" "chain-critique" + echo "Chaining critique stage with refine run ID: ${THIS_RUN_ID}" + + CURRENT_TRACEPARENT=$(get_traceparent) + + CHAIN_ARGS=( + --repo "$WORKFLOW_REPO" + -f issue_key="${ISSUE_KEY}" + -f issue_source="${ISSUE_SOURCE}" + -f refine_run_id="${THIS_RUN_ID}" + -f review_round="${REVIEW_ROUND}" + -f max_review_rounds="${MAX_REVIEW_ROUNDS}" + -f auto_create="${AUTO_CREATE}" + ) + + if [[ -n "$TARGET_REPO" ]]; then + CHAIN_ARGS+=(-f repo_full_name="${TARGET_REPO}") + echo "Propagating target repo: ${TARGET_REPO}" + fi + + if [[ -n "$CURRENT_TRACEPARENT" ]]; then + CHAIN_ARGS+=(-f parent_traceparent="${CURRENT_TRACEPARENT}") + echo "Propagating trace context: ${CURRENT_TRACEPARENT}" + fi + + if [[ -n "${GITHUB_ISSUE_NUMBER:-}" && "${GITHUB_ISSUE_NUMBER}" != "N/A" ]]; then + CHAIN_ARGS+=(-f github_issue_number="${GITHUB_ISSUE_NUMBER}") + fi + + gh workflow run critique.yml "${CHAIN_ARGS[@]}" \ + 2>/dev/null || echo "::warning::Failed to chain critique workflow — trigger manually" + pe_end "post-refine" "chain-critique" "$(jq -nc --arg run_id "$THIS_RUN_ID" --arg traceparent "$CURRENT_TRACEPARENT" --argjson round "$REVIEW_ROUND" '{refine_run_id:$run_id, traceparent:$traceparent, review_round:$round}')" +else + echo "::warning::GITHUB_RUN_ID not available — critique must be triggered manually" +fi + +pe_end "post-refine" "post-plan" "$(jq -nc --argjson total "$CHILD_COUNT" --argjson epics "$EPIC_COUNT" --argjson stories "$STORY_COUNT" --argjson tasks "$TASK_COUNT" --argjson open_questions "$OPEN_QUESTION_COUNT" '{total:$total, epics:$epics, stories:$stories, tasks:$tasks, open_questions:$open_questions}')" + +pe_end "post-refine" "post-refine" "$(jq -nc --arg status "$STATUS" --argjson confidence "$CONFIDENCE_INT" --argjson round "$REVIEW_ROUND" '{status:$status, confidence:$confidence, review_round:$round}')" +pe_copy_to_output + +echo "Post-refine complete." diff --git a/internal/scaffold/fullsend-repo/scripts/pre-critique.sh b/internal/scaffold/fullsend-repo/scripts/pre-critique.sh new file mode 100755 index 000000000..5555e42e6 --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/pre-critique.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# pre-critique.sh — Prepare context for the critique agent. +# +# Downloads the refine agent's result and assembles the full context +# (issue, exploration, refinement plan, prior critique history) for review. +# +# Required env vars: +# ISSUE_KEY — Issue identifier +# ISSUE_SOURCE — "jira" or "github" +# REFINE_RUN_ID — GitHub Actions run ID of the refine stage +# GH_TOKEN — GitHub token +# +# Optional env vars: +# REVIEW_ROUND — Current review round (default: 1) +# MAX_REVIEW_ROUNDS — Max rounds before escalation (default: 3) +# GITHUB_ISSUE_NUMBER — GitHub issue for reply-back +# JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN — for Jira sources +# REPO_FULL_NAME — for GitHub sources + +set -euo pipefail + +WORKSPACE="/tmp/workspace" +mkdir -p "$WORKSPACE" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/pipeline-events.sh" + +pe_start "pre-critique" "pre-critique" + +REVIEW_ROUND="${REVIEW_ROUND:-1}" +MAX_REVIEW_ROUNDS="${MAX_REVIEW_ROUNDS:-3}" + +echo "::notice::Pre-critique: preparing context (source=${ISSUE_SOURCE}, key=${ISSUE_KEY}, round=${REVIEW_ROUND}/${MAX_REVIEW_ROUNDS})" + +# --- Step 1: Ensure issue context --- +pe_start "pre-critique" "fetch-issue-context" +if [[ ! -f "$WORKSPACE/issue-context.json" ]]; then + if [[ -f "${SCRIPT_DIR}/pre-explore.sh" ]]; then + echo "Fetching issue context via pre-explore.sh..." + bash "${SCRIPT_DIR}/pre-explore.sh" + else + echo "ERROR: No issue context available and pre-explore.sh not found" + exit 1 + fi +fi +pe_end "pre-critique" "fetch-issue-context" '{}' + +# --- Step 2: Download refine result --- +pe_start "pre-critique" "fetch-refine-result" + +# Check for refine-result.json already present (from post-refine.sh copy), +# or extract from downloaded artifact iteration layout +if [[ -f "$WORKSPACE/refine-result.json" ]]; then + echo "Refine result already present." +elif ls "$WORKSPACE"/iteration-*/output/agent-result.json 1>/dev/null 2>&1; then + # Artifact was downloaded directly to workspace — extract the result + for dir in "$WORKSPACE"/iteration-*/output; do + if [[ -f "${dir}/agent-result.json" ]]; then + cp "${dir}/agent-result.json" "$WORKSPACE/refine-result.json" + fi + done + echo "Refine result extracted from downloaded artifact." +elif [[ -n "${REFINE_RUN_ID:-}" && "${REFINE_RUN_ID}" != "N/A" ]]; then + REPO="${REPO_FULL_NAME:-$(gh api repos/:owner/:repo --jq .full_name 2>/dev/null || echo "")}" + if [[ -n "$REPO" ]]; then + echo "Downloading refine artifact from run ${REFINE_RUN_ID}..." + ARTIFACT_DIR=$(mktemp -d) + if gh run download "$REFINE_RUN_ID" --repo "$REPO" --name "fullsend-refine" --dir "$ARTIFACT_DIR" 2>/dev/null; then + # Find the agent-result.json in the artifact + REFINE_RESULT_IN_ARTIFACT="" + for dir in "$ARTIFACT_DIR"/iteration-*/output; do + if [[ -f "${dir}/agent-result.json" ]]; then + REFINE_RESULT_IN_ARTIFACT="${dir}/agent-result.json" + fi + done + + if [[ -n "$REFINE_RESULT_IN_ARTIFACT" ]]; then + cp "$REFINE_RESULT_IN_ARTIFACT" "$WORKSPACE/refine-result.json" + echo "Refine result extracted from artifact." + else + echo "::error::Refine artifact downloaded but agent-result.json not found" + exit 1 + fi + + # Also grab exploration context and issue context if present + for f in exploration_context.json issue-context.json; do + if [[ -f "$ARTIFACT_DIR/$f" && ! -f "$WORKSPACE/$f" ]]; then + cp "$ARTIFACT_DIR/$f" "$WORKSPACE/$f" + fi + done + else + echo "::error::Could not download refine artifact from run ${REFINE_RUN_ID}" + exit 1 + fi + rm -rf "$ARTIFACT_DIR" + fi +else + echo "::error::No refine result available (REFINE_RUN_ID not set)" + exit 1 +fi + +if ! jq empty "$WORKSPACE/refine-result.json" 2>/dev/null; then + echo "::error::Refine result is not valid JSON" + exit 1 +fi + +pe_end "pre-critique" "fetch-refine-result" '{}' + +# --- Step 3: Build critique history for round 2+ --- +pe_start "pre-critique" "build-critique-history" + +if [[ "$REVIEW_ROUND" -gt 1 && ! -f "$WORKSPACE/critique-history.json" ]]; then + echo "Round ${REVIEW_ROUND}: building critique history from prior rounds..." + # History is accumulated by post-critique.sh and passed as an artifact. + # If it's not already present from artifact download, create empty placeholder. + echo '{"rounds": [], "note": "History not available from artifact — critique agent should focus on current plan quality"}' \ + > "$WORKSPACE/critique-history.json" +fi + +if [[ ! -f "$WORKSPACE/critique-history.json" ]]; then + echo '{"rounds": []}' > "$WORKSPACE/critique-history.json" +fi + +pe_end "pre-critique" "build-critique-history" "$(jq -nc --argjson round "$REVIEW_ROUND" '{review_round:$round}')" + +# --- Step 4: Ensure exploration context --- +if [[ ! -f "$WORKSPACE/exploration_context.json" ]]; then + echo "::warning::No exploration context available — critique will rely on issue context and refine result" + echo '{"gaps": [{"dimension": "exploration", "description": "Explore stage context not available to critique"}], "confidence": {"overall": 50}}' \ + > "$WORKSPACE/exploration_context.json" +fi + +# --- Export paths --- +echo "ISSUE_CONTEXT=$WORKSPACE/issue-context.json" >> "${GITHUB_ENV:-/dev/null}" +echo "EXPLORE_CONTEXT=$WORKSPACE/exploration_context.json" >> "${GITHUB_ENV:-/dev/null}" +echo "REFINE_RESULT=$WORKSPACE/refine-result.json" >> "${GITHUB_ENV:-/dev/null}" +echo "CRITIQUE_HISTORY=$WORKSPACE/critique-history.json" >> "${GITHUB_ENV:-/dev/null}" +echo "REVIEW_ROUND=$REVIEW_ROUND" >> "${GITHUB_ENV:-/dev/null}" +echo "MAX_REVIEW_ROUNDS=$MAX_REVIEW_ROUNDS" >> "${GITHUB_ENV:-/dev/null}" + +pe_end "pre-critique" "pre-critique" "$(jq -nc --arg source "$ISSUE_SOURCE" --arg key "$ISSUE_KEY" --argjson round "$REVIEW_ROUND" '{source:$source, key:$key, review_round:$round}')" + +echo "Pre-critique complete." diff --git a/internal/scaffold/fullsend-repo/scripts/pre-explore.sh b/internal/scaffold/fullsend-repo/scripts/pre-explore.sh new file mode 100755 index 000000000..f943be49c --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/pre-explore.sh @@ -0,0 +1,313 @@ +#!/usr/bin/env bash +# pre-explore.sh — Fetch issue data and prepare context for the explore agent. +# +# Runs on the host before the sandbox is created. Fetches issue data from +# Jira or GitHub using credentials that never enter the sandbox. +# +# Required env vars: +# ISSUE_KEY — Jira key (e.g., SECURESIGN-1620) or GitHub issue number +# ISSUE_SOURCE — "jira" or "github" +# GH_TOKEN — GitHub token +# +# Jira-only env vars: +# JIRA_HOST — Jira hostname (e.g., stage-redhat.atlassian.net) +# JIRA_EMAIL — Jira user email +# JIRA_API_TOKEN — Jira API token +# +# GitHub-only env vars: +# REPO_FULL_NAME — owner/repo (e.g., fullsend-ai/features) + +set -euo pipefail + +WORKSPACE="/tmp/workspace" +mkdir -p "$WORKSPACE" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/pipeline-events.sh" + +pe_start "pre-explore" "pre-explore" + +echo "::notice::Pre-explore: fetching issue data (source=${ISSUE_SOURCE}, key=${ISSUE_KEY})" + +# --- Safety check: block private Jira on public repos --- +# The risk: private Jira data leaking into public GitHub Actions artifacts/logs. +# This only matters when the Jira project is private. Public Jira on a public +# repo is fine — the data is already public. +# +# JIRA_PROJECT_VISIBILITY controls this. Values: +# "public" — Jira project is publicly accessible, skip the check +# "private" — Jira project is private, require a private config repo (default) +# unset — treated as "private" (safe default) +if [[ "${ISSUE_SOURCE}" == "jira" ]]; then + JIRA_VIS="${JIRA_PROJECT_VISIBILITY:-private}" + if [[ "$JIRA_VIS" != "public" ]]; then + CONFIG_REPO="${GITHUB_REPOSITORY:-}" + if [[ -n "$CONFIG_REPO" ]]; then + REPO_VISIBILITY=$(gh api "repos/${CONFIG_REPO}" --jq '.visibility' 2>/dev/null || echo "unknown") + if [[ "$REPO_VISIBILITY" == "public" ]]; then + echo "::error::SECURITY: Private Jira source blocked — this repo (${CONFIG_REPO}) is public." + echo "::error::Private Jira data would leak into public workflow artifacts and logs." + echo "::error::Options:" + echo "::error:: 1. Run fullsend from a PRIVATE config repo" + echo "::error:: 2. Set JIRA_PROJECT_VISIBILITY=public if this Jira project is publicly accessible" + echo "::error:: 3. Use ISSUE_SOURCE=github instead" + exit 1 + fi + fi + fi +fi + +# Validate inputs to prevent injection +if [[ "${ISSUE_SOURCE}" != "jira" && "${ISSUE_SOURCE}" != "github" ]]; then + echo "ERROR: ISSUE_SOURCE must be 'jira' or 'github', got: ${ISSUE_SOURCE}" + exit 1 +fi + +if [[ "${ISSUE_SOURCE}" == "jira" && ! "${ISSUE_KEY}" =~ ^[A-Z][A-Z0-9]+-[0-9]+$ ]]; then + echo "ERROR: ISSUE_KEY does not match Jira key pattern (e.g., PROJECT-123): ${ISSUE_KEY}" + exit 1 +fi + +if [[ "${ISSUE_SOURCE}" == "github" && ! "${ISSUE_KEY}" =~ ^[0-9]+$ ]]; then + echo "ERROR: ISSUE_KEY must be a numeric GitHub issue number, got: ${ISSUE_KEY}" + exit 1 +fi + +if [[ "${ISSUE_SOURCE}" == "jira" ]]; then + if [[ -z "${JIRA_HOST:-}" || -z "${JIRA_EMAIL:-}" || -z "${JIRA_API_TOKEN:-}" ]]; then + echo "ERROR: Jira credentials not set (JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN)" + exit 1 + fi + + JIRA_BASE="https://${JIRA_HOST}/rest/api/3" + AUTH=$(printf '%s:%s' "$JIRA_EMAIL" "$JIRA_API_TOKEN" | base64 -w0) + + jira_get() { + curl -sSf -H "Authorization: Basic $AUTH" \ + -H "Accept: application/json" "$1" + } + + pe_start "pre-explore" "fetch-issue" + ISSUE_JSON=$(jira_get "${JIRA_BASE}/issue/${ISSUE_KEY}?expand=names") + + SUMMARY=$(echo "$ISSUE_JSON" | jq -r '.fields.summary // ""') + DESCRIPTION=$(echo "$ISSUE_JSON" | jq -r '.fields.description // "" | if type == "object" then (.content // [] | map(.content // [] | map(.text // "") | join("")) | join("\n")) else . end') + STATUS=$(echo "$ISSUE_JSON" | jq -r '.fields.status.name // ""') + PRIORITY=$(echo "$ISSUE_JSON" | jq -r '.fields.priority.name // ""') + ISSUE_TYPE=$(echo "$ISSUE_JSON" | jq -r '.fields.issuetype.name // ""') + REPORTER=$(echo "$ISSUE_JSON" | jq -r '.fields.reporter.emailAddress // ""') + LABELS=$(echo "$ISSUE_JSON" | jq -c '.fields.labels // []') + CREATED=$(echo "$ISSUE_JSON" | jq -r '.fields.created // ""') + UPDATED=$(echo "$ISSUE_JSON" | jq -r '.fields.updated // ""') + + # Determine level from issue type + LEVEL="issue" + case "${ISSUE_TYPE,,}" in + outcome) LEVEL="outcome" ;; + feature) LEVEL="feature" ;; + epic) LEVEL="epic" ;; + story) LEVEL="story" ;; + task|sub-task) LEVEL="task" ;; + esac + + pe_end "pre-explore" "fetch-issue" "$(jq -nc --arg key "$ISSUE_KEY" --arg type "$ISSUE_TYPE" --arg level "$LEVEL" --arg status "$STATUS" '{key:$key, type:$type, level:$level, status:$status}')" + + pe_start "pre-explore" "fetch-parent" + PARENT_KEY=$(echo "$ISSUE_JSON" | jq -r '.fields.parent.key // ""') + PARENT_JSON="null" + if [[ -n "$PARENT_KEY" ]]; then + PARENT_ISSUE=$(jira_get "${JIRA_BASE}/issue/${PARENT_KEY}" 2>/dev/null || echo "{}") + PARENT_SUMMARY=$(echo "$PARENT_ISSUE" | jq -r '.fields.summary // ""') + PARENT_DESC=$(echo "$PARENT_ISSUE" | jq -r '.fields.description // "" | if type == "object" then (.content // [] | map(.content // [] | map(.text // "") | join("")) | join("\n")) else . end') + PARENT_JSON=$(jq -n --arg k "$PARENT_KEY" --arg s "$PARENT_SUMMARY" --arg d "$PARENT_DESC" \ + '{"key": $k, "summary": $s, "description": $d}') + fi + + pe_end "pre-explore" "fetch-parent" "$(jq -nc --arg parent_key "${PARENT_KEY:-none}" '{parent_key:$parent_key}')" + + pe_start "pre-explore" "fetch-children" + CHILDREN_JSON=$(jira_get "${JIRA_BASE}/search?jql=parent=${ISSUE_KEY}&fields=summary,status,issuetype&maxResults=50" 2>/dev/null \ + | jq '[.issues[] | {key: .key, summary: .fields.summary, status: .fields.status.name, type: .fields.issuetype.name}]' \ + || echo "[]") + + CHILD_COUNT=$(echo "$CHILDREN_JSON" | jq 'length') + pe_end "pre-explore" "fetch-children" "$(jq -nc --argjson count "$CHILD_COUNT" '{child_count:$count}')" + + pe_start "pre-explore" "fetch-comments" + COMMENTS_JSON=$(jira_get "${JIRA_BASE}/issue/${ISSUE_KEY}/comment?maxResults=50" 2>/dev/null \ + | jq '[.comments[] | {author: .author.emailAddress, created: .created, body: (.body | if type == "object" then (.content // [] | map(.content // [] | map(.text // "") | join("")) | join("\n")) else . end)}]' \ + || echo "[]") + + COMMENT_COUNT=$(echo "$COMMENTS_JSON" | jq 'length') + pe_end "pre-explore" "fetch-comments" "$(jq -nc --argjson count "$COMMENT_COUNT" '{comment_count:$count}')" + + pe_start "pre-explore" "fetch-links-and-project" + LINKS_JSON=$(echo "$ISSUE_JSON" | jq '[.fields.issuelinks // [] | .[] | { + type: (.type.outward // .type.name), + key: (.outwardIssue.key // .inwardIssue.key), + summary: (.outwardIssue.fields.summary // .inwardIssue.fields.summary), + status: (.outwardIssue.fields.status.name // .inwardIssue.fields.status.name) + }]') + + # Fetch project metadata + PROJECT_KEY=$(echo "$ISSUE_JSON" | jq -r '.fields.project.key') + PROJECT_NAME=$(echo "$ISSUE_JSON" | jq -r '.fields.project.name') + + # Fetch available issue types for the project + PROJECT_ISSUE_TYPES=$(jira_get "${JIRA_BASE}/project/${PROJECT_KEY}" 2>/dev/null \ + | jq '[.issueTypes[]? | {name: .name, subtask: .subtask, hierarchyLevel: .hierarchyLevel, description: .description}]' \ + || echo "[]") + + # If project endpoint didn't return issue types, try createmeta + if [[ "$PROJECT_ISSUE_TYPES" == "[]" || "$PROJECT_ISSUE_TYPES" == "null" ]]; then + PROJECT_ISSUE_TYPES=$(jira_get "${JIRA_BASE}/issue/createmeta?projectKeys=${PROJECT_KEY}&expand=projects.issuetypes" 2>/dev/null \ + | jq '[.projects[0].issuetypes[]? | {name: .name, subtask: .subtask, hierarchyLevel: .hierarchyLevel, description: .description}]' \ + || echo "[]") + fi + + echo "Available issue types for ${PROJECT_KEY}: $(echo "$PROJECT_ISSUE_TYPES" | jq -r '[.[].name] | join(", ")')" + + # Sample existing children to learn team conventions (type distribution) + TEAM_USAGE=$(jira_get "${JIRA_BASE}/search?jql=project=${PROJECT_KEY}+AND+issuetype+in+(Story,Task,Epic,Feature,Bug,Spike)+ORDER+BY+created+DESC&fields=issuetype,labels&maxResults=50" 2>/dev/null \ + | jq '{ + type_counts: [.issues[]? | .fields.issuetype.name] | group_by(.) | map({type: .[0], count: length}) | sort_by(-.count), + common_labels: [.issues[]? | .fields.labels[]?] | group_by(.) | map({label: .[0], count: length}) | sort_by(-.count) | .[0:10] + }' \ + || echo '{"type_counts": [], "common_labels": []}') + + LINK_COUNT=$(echo "$LINKS_JSON" | jq 'length') + TYPE_COUNT=$(echo "$PROJECT_ISSUE_TYPES" | jq 'length') + pe_end "pre-explore" "fetch-links-and-project" "$(jq -nc --argjson links "$LINK_COUNT" --argjson types "$TYPE_COUNT" --arg proj "$PROJECT_KEY" '{link_count:$links, issue_type_count:$types, project:$proj}')" + + pe_start "pre-explore" "build-context" + jq -n \ + --arg source "jira" \ + --arg host "$JIRA_HOST" \ + --arg key "$ISSUE_KEY" \ + --arg level "$LEVEL" \ + --arg summary "$SUMMARY" \ + --arg description "$DESCRIPTION" \ + --arg status "$STATUS" \ + --arg priority "$PRIORITY" \ + --argjson labels "$LABELS" \ + --arg reporter "$REPORTER" \ + --arg created "$CREATED" \ + --arg updated "$UPDATED" \ + --argjson parent "$PARENT_JSON" \ + --argjson children "$CHILDREN_JSON" \ + --argjson comments "$COMMENTS_JSON" \ + --argjson linked_issues "$LINKS_JSON" \ + --arg project_key "$PROJECT_KEY" \ + --arg project_name "$PROJECT_NAME" \ + --argjson available_issue_types "$PROJECT_ISSUE_TYPES" \ + --argjson team_usage "$TEAM_USAGE" \ + '{ + source: $source, + host: $host, + key: $key, + level: $level, + summary: $summary, + description: $description, + status: $status, + priority: $priority, + labels: $labels, + reporter: $reporter, + created: $created, + updated: $updated, + parent: $parent, + children: $children, + comments: $comments, + linked_issues: $linked_issues, + project: {key: $project_key, name: $project_name, available_issue_types: $available_issue_types, team_usage: $team_usage} + }' > "$WORKSPACE/issue-context.json" + +elif [[ "${ISSUE_SOURCE}" == "github" ]]; then + if [[ -z "${REPO_FULL_NAME:-}" ]]; then + echo "ERROR: REPO_FULL_NAME not set for GitHub source" + exit 1 + fi + + pe_start "pre-explore" "fetch-issue" + ISSUE_JSON=$(gh issue view "$ISSUE_KEY" --repo "$REPO_FULL_NAME" \ + --json number,title,body,labels,comments,state,milestone,assignees,createdAt,updatedAt,author) + + TITLE=$(echo "$ISSUE_JSON" | jq -r '.title') + BODY=$(echo "$ISSUE_JSON" | jq -r '.body // ""') + STATE=$(echo "$ISSUE_JSON" | jq -r '.state') + LABELS=$(echo "$ISSUE_JSON" | jq -c '[.labels[].name]') + AUTHOR=$(echo "$ISSUE_JSON" | jq -r '.author.login') + CREATED=$(echo "$ISSUE_JSON" | jq -r '.createdAt') + UPDATED=$(echo "$ISSUE_JSON" | jq -r '.updatedAt') + + # Determine level from labels + LEVEL="issue" + while IFS= read -r label; do + case "${label,,}" in + feature) LEVEL="feature" ;; + epic) LEVEL="epic" ;; + story) LEVEL="story" ;; + task) LEVEL="task" ;; + esac + done < <(echo "$LABELS" | jq -r '.[]') + + COMMENT_COUNT=$(echo "$ISSUE_JSON" | jq '.comments | length') + pe_end "pre-explore" "fetch-issue" "$(jq -nc --arg key "#${ISSUE_KEY}" --arg level "$LEVEL" --arg status "$STATE" --argjson comments "$COMMENT_COUNT" '{key:$key, level:$level, status:$status, comment_count:$comments}')" + + pe_start "pre-explore" "fetch-comments" + COMMENTS=$(echo "$ISSUE_JSON" | jq '[.comments[] | {author: .author.login, created: .createdAt, body: .body}]') + pe_end "pre-explore" "fetch-comments" "$(jq -nc --argjson count "$COMMENT_COUNT" '{comment_count:$count}')" + + pe_start "pre-explore" "fetch-children" + SUB_ISSUES=$(gh issue list --repo "$REPO_FULL_NAME" --state all \ + --search "parent:#${ISSUE_KEY}" --json number,title,state,labels --limit 30 2>/dev/null \ + | jq '[.[] | {key: ("#" + (.number | tostring)), summary: .title, status: .state, type: "issue"}]' \ + || echo "[]") + CHILD_COUNT=$(echo "$SUB_ISSUES" | jq 'length') + pe_end "pre-explore" "fetch-children" "$(jq -nc --argjson count "$CHILD_COUNT" '{child_count:$count}')" + + jq -n \ + --arg source "github" \ + --arg key "#${ISSUE_KEY}" \ + --arg level "$LEVEL" \ + --arg summary "$TITLE" \ + --arg description "$BODY" \ + --arg status "$STATE" \ + --argjson labels "$LABELS" \ + --arg reporter "$AUTHOR" \ + --arg created "$CREATED" \ + --arg updated "$UPDATED" \ + --argjson children "$SUB_ISSUES" \ + --argjson comments "$COMMENTS" \ + --arg repo "$REPO_FULL_NAME" \ + '{ + source: $source, + key: $key, + level: $level, + summary: $summary, + description: $description, + status: $status, + labels: $labels, + reporter: $reporter, + created: $created, + updated: $updated, + parent: null, + children: $children, + comments: $comments, + linked_issues: [], + project: {key: $repo, name: $repo} + }' > "$WORKSPACE/issue-context.json" + +else + echo "ERROR: Unknown ISSUE_SOURCE: ${ISSUE_SOURCE}" + exit 1 +fi + +pe_end "pre-explore" "build-context" '{}' + +echo "Issue context written to $WORKSPACE/issue-context.json" +echo "::notice::Issue: ${ISSUE_KEY} (${ISSUE_SOURCE}, level=$(jq -r .level "$WORKSPACE/issue-context.json"))" + +pe_end "pre-explore" "pre-explore" "$(jq -nc --arg source "$ISSUE_SOURCE" --arg key "$ISSUE_KEY" --arg level "$(jq -r .level "$WORKSPACE/issue-context.json")" '{source:$source, key:$key, level:$level}')" + +# Export paths for the agent +echo "ISSUE_CONTEXT=$WORKSPACE/issue-context.json" >> "${GITHUB_ENV:-/dev/null}" diff --git a/internal/scaffold/fullsend-repo/scripts/pre-refine.sh b/internal/scaffold/fullsend-repo/scripts/pre-refine.sh new file mode 100755 index 000000000..eef263d91 --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/pre-refine.sh @@ -0,0 +1,188 @@ +#!/usr/bin/env bash +# pre-refine.sh — Prepare context for the refine agent. +# +# Fetches issue data (if not already available) and downloads/locates +# the exploration context from one of three sources: +# 1. Explore workflow artifact (EXPLORE_RUN_ID) +# 2. User-provided file from a repo (EXPLORE_CONTEXT_REF = owner/repo:path) +# 3. User-provided file from a Jira attachment (EXPLORE_CONTEXT_REF = attachment name) +# +# Required env vars: +# ISSUE_KEY — Issue identifier +# ISSUE_SOURCE — "jira" or "github" +# GH_TOKEN — GitHub token +# +# Optional env vars: +# EXPLORE_RUN_ID — GitHub Actions run ID of the explore stage +# EXPLORE_CONTEXT_REF — User-provided exploration context reference +# CRITIQUE_RUN_ID — GitHub Actions run ID of the critique stage (revision rounds) +# REVIEW_ROUND — Current review round (default: 1) +# GITHUB_ISSUE_NUMBER — GitHub issue for reply-back (GitHub flow) +# JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN — for Jira sources +# REPO_FULL_NAME — for GitHub sources + +set -euo pipefail + +WORKSPACE="/tmp/workspace" +mkdir -p "$WORKSPACE" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/pipeline-events.sh" + +pe_start "pre-refine" "pre-refine" + +echo "::notice::Pre-refine: preparing context (source=${ISSUE_SOURCE}, key=${ISSUE_KEY})" + +pe_start "pre-refine" "fetch-issue-context" +if [[ ! -f "$WORKSPACE/issue-context.json" ]]; then + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + if [[ -f "${SCRIPT_DIR}/pre-explore.sh" ]]; then + echo "Fetching issue context via pre-explore.sh..." + bash "${SCRIPT_DIR}/pre-explore.sh" + else + echo "ERROR: No issue context available and pre-explore.sh not found" + exit 1 + fi +fi + +pe_end "pre-refine" "fetch-issue-context" '{}' + +pe_start "pre-refine" "obtain-exploration-context" + +if [[ -f "$WORKSPACE/exploration_context.json" ]]; then + echo "Exploration context already present." + +elif [[ -n "${EXPLORE_CONTEXT_REF:-}" && "${EXPLORE_CONTEXT_REF}" != "N/A" ]]; then + # User-provided exploration context (skip-explore flow) + echo "Fetching user-provided exploration context: ${EXPLORE_CONTEXT_REF}" + + if [[ "$EXPLORE_CONTEXT_REF" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+:.+ ]]; then + # Format: owner/repo:path/to/file.json — fetch from a GitHub repo + CONTEXT_REPO="${EXPLORE_CONTEXT_REF%%:*}" + CONTEXT_PATH="${EXPLORE_CONTEXT_REF#*:}" + + echo " Fetching from GitHub repo: ${CONTEXT_REPO} path: ${CONTEXT_PATH}" + gh api "repos/${CONTEXT_REPO}/contents/${CONTEXT_PATH}" \ + --jq '.content' | base64 -d > "$WORKSPACE/exploration_context.json" \ + || { echo "::error::Failed to fetch exploration context from ${CONTEXT_REPO}:${CONTEXT_PATH}"; exit 1; } + + elif [[ "$EXPLORE_CONTEXT_REF" =~ ^https?:// ]]; then + # Direct URL + echo " Fetching from URL: ${EXPLORE_CONTEXT_REF}" + curl -sSfL "$EXPLORE_CONTEXT_REF" > "$WORKSPACE/exploration_context.json" \ + || { echo "::error::Failed to fetch exploration context from URL"; exit 1; } + + elif [[ "${ISSUE_SOURCE}" == "jira" && -n "${JIRA_HOST:-}" ]]; then + # Treat as Jira attachment name + ATTACHMENT_NAME="$EXPLORE_CONTEXT_REF" + echo " Fetching Jira attachment: ${ATTACHMENT_NAME}" + + AUTH=$(printf '%s:%s' "$JIRA_EMAIL" "$JIRA_API_TOKEN" | base64 -w0) + ATTACHMENT_URL=$(curl -sSf \ + -H "Authorization: Basic $AUTH" \ + -H "Accept: application/json" \ + "https://${JIRA_HOST}/rest/api/3/issue/${ISSUE_KEY}?fields=attachment" \ + | jq -r --arg name "$ATTACHMENT_NAME" \ + '.fields.attachment[] | select(.filename == $name) | .content' \ + | head -1) + + if [[ -z "$ATTACHMENT_URL" ]]; then + echo "::error::Jira attachment '${ATTACHMENT_NAME}' not found on ${ISSUE_KEY}" + exit 1 + fi + + curl -sSfL -H "Authorization: Basic $AUTH" \ + "$ATTACHMENT_URL" > "$WORKSPACE/exploration_context.json" \ + || { echo "::error::Failed to download Jira attachment"; exit 1; } + + else + echo "::error::Cannot resolve EXPLORE_CONTEXT_REF: ${EXPLORE_CONTEXT_REF}" + exit 1 + fi + + # Validate the fetched context + if ! jq empty "$WORKSPACE/exploration_context.json" 2>/dev/null; then + echo "::error::Fetched exploration context is not valid JSON" + exit 1 + fi + + echo "User-provided exploration context loaded." + +elif [[ -n "${EXPLORE_RUN_ID:-}" && "${EXPLORE_RUN_ID}" != "N/A" ]]; then + # Download from explore workflow artifact + REPO="${REPO_FULL_NAME:-$(gh api repos/:owner/:repo --jq .full_name 2>/dev/null || echo "")}" + if [[ -n "$REPO" ]]; then + echo "Downloading exploration artifact from run ${EXPLORE_RUN_ID}..." + ARTIFACT_DIR=$(mktemp -d) + if gh run download "$EXPLORE_RUN_ID" --repo "$REPO" --name "fullsend-explore" --dir "$ARTIFACT_DIR" 2>/dev/null; then + if [[ -f "$ARTIFACT_DIR/exploration_context.json" ]]; then + cp "$ARTIFACT_DIR/exploration_context.json" "$WORKSPACE/exploration_context.json" + echo "Exploration context downloaded from explore run." + fi + else + echo "::warning::Could not download exploration artifact — refine will proceed without it" + fi + rm -rf "$ARTIFACT_DIR" + fi +fi + +pe_end "pre-refine" "obtain-exploration-context" "$(jq -nc --argjson has_context "$(if [[ -f "$WORKSPACE/exploration_context.json" ]]; then echo true; else echo false; fi)" '{has_exploration_context:$has_context}')" + +# Step 3: If no exploration context, create a minimal placeholder +if [[ ! -f "$WORKSPACE/exploration_context.json" ]]; then + echo "::warning::No exploration context available — refine agent will rely on issue context and codebase only" + echo '{"gaps": [{"dimension": "exploration", "description": "Explore stage did not run", "impact": "Refine agent has limited context"}], "confidence": {"overall": 50}}' \ + > "$WORKSPACE/exploration_context.json" +fi + +# --- Step 4: Load critique feedback for revision rounds --- +REVIEW_ROUND="${REVIEW_ROUND:-1}" + +if [[ "$REVIEW_ROUND" -gt 1 && -n "${CRITIQUE_RUN_ID:-}" && "${CRITIQUE_RUN_ID}" != "N/A" ]]; then + pe_start "pre-refine" "fetch-critique-feedback" + echo "Revision round ${REVIEW_ROUND}: downloading critique feedback from run ${CRITIQUE_RUN_ID}..." + + REPO="${REPO_FULL_NAME:-$(gh api repos/:owner/:repo --jq .full_name 2>/dev/null || echo "")}" + if [[ -n "$REPO" ]]; then + ARTIFACT_DIR=$(mktemp -d) + if gh run download "$CRITIQUE_RUN_ID" --repo "$REPO" --name "fullsend-critique" --dir "$ARTIFACT_DIR" 2>/dev/null; then + # Find the critique result + CRITIQUE_RESULT_IN_ARTIFACT="" + for dir in "$ARTIFACT_DIR"/iteration-*/output; do + if [[ -f "${dir}/agent-result.json" ]]; then + CRITIQUE_RESULT_IN_ARTIFACT="${dir}/agent-result.json" + fi + done + + if [[ -n "$CRITIQUE_RESULT_IN_ARTIFACT" ]]; then + cp "$CRITIQUE_RESULT_IN_ARTIFACT" "$WORKSPACE/critique-feedback.json" + echo "Critique feedback loaded." + fi + + # Also grab critique history and exploration context if present + for f in critique-history.json exploration_context.json issue-context.json; do + if [[ -f "$ARTIFACT_DIR/$f" && ! -f "$WORKSPACE/$f" ]]; then + cp "$ARTIFACT_DIR/$f" "$WORKSPACE/$f" + fi + done + else + echo "::warning::Could not download critique artifact — refine will proceed without feedback" + fi + rm -rf "$ARTIFACT_DIR" + fi + pe_end "pre-refine" "fetch-critique-feedback" "$(jq -nc --argjson round "$REVIEW_ROUND" '{review_round:$round}')" +elif [[ -f "$WORKSPACE/critique-feedback.json" ]]; then + echo "Critique feedback already present from artifact download." +fi + +echo "ISSUE_CONTEXT=$WORKSPACE/issue-context.json" >> "${GITHUB_ENV:-/dev/null}" +echo "EXPLORE_CONTEXT=$WORKSPACE/exploration_context.json" >> "${GITHUB_ENV:-/dev/null}" +echo "REVIEW_ROUND=$REVIEW_ROUND" >> "${GITHUB_ENV:-/dev/null}" + +if [[ -f "$WORKSPACE/critique-feedback.json" ]]; then + echo "CRITIQUE_FEEDBACK=$WORKSPACE/critique-feedback.json" >> "${GITHUB_ENV:-/dev/null}" +fi + +pe_end "pre-refine" "pre-refine" "$(jq -nc --arg source "$ISSUE_SOURCE" --arg key "$ISSUE_KEY" --argjson round "$REVIEW_ROUND" '{source:$source, key:$key, review_round:$round}')" + +echo "Pre-refine complete." diff --git a/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts-test.sh b/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts-test.sh new file mode 100755 index 000000000..bf289ab45 --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts-test.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# sanitize-artifacts-test.sh — Test sanitize-artifacts.sh redaction logic. +# +# Run from the repo root: bash internal/scaffold/fullsend-repo/scripts/sanitize-artifacts-test.sh + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SANITIZE_SCRIPT="${SCRIPT_DIR}/sanitize-artifacts.sh" +FAILURES=0 + +TEST_TMPDIR="$(mktemp -d)" +trap 'rm -rf "${TEST_TMPDIR}"' EXIT + +# --- Fixtures --- + +# Transcript JSONL with private data +TRANSCRIPT_FIXTURE='{"message":{"content":[{"type":"tool_result","content":"{\"source\":\"jira\",\"key\":\"PROJ-123\",\"summary\":\"Add login\",\"description\":\"Private issue body with secrets\",\"reporter\":\"alice@example.com\",\"host\":\"redhat.atlassian.net\",\"comments\":[{\"author\":\"bob@corp.com\",\"body\":\"internal comment\"}],\"linked_issues\":[{\"key\":\"PROJ-456\",\"description\":\"linked desc\"}],\"parent\":{\"key\":\"PROJ-100\",\"description\":\"parent body\"}}"},{"type":"text","text":"User alice@example.com reported this on redhat.atlassian.net"},{"type":"thinking","thinking":"I see https://redhat-internal.slack.com/archives/C123 is related"},{"type":"tool_use","input":{"query":"check https://docs.google.com/document/d/secret123"}}]}}' + +# agent-result.json with emails +RESULT_FIXTURE='{ + "summary": "Found issue at staging.atlassian.net reported by dev@company.com", + "confidence": {"overall": 85}, + "technical_landscape": { + "overview": "Check https://redhat-internal.slack.com/foo for details" + } +}' + +run_test() { + local test_name="$1" + + local input_dir="${TEST_TMPDIR}/input-${test_name}" + local output_dir="${TEST_TMPDIR}/output-${test_name}" + mkdir -p "${input_dir}/iteration-1/output" + + echo "${TRANSCRIPT_FIXTURE}" > "${input_dir}/iteration-1/transcript.jsonl" + echo "${RESULT_FIXTURE}" > "${input_dir}/iteration-1/output/agent-result.json" + + local exit_code=0 + bash "${SANITIZE_SCRIPT}" "${input_dir}" "${output_dir}" > "${TEST_TMPDIR}/stdout-${test_name}.log" 2>&1 || exit_code=$? + + if [[ ${exit_code} -ne 0 ]]; then + echo "FAIL: ${test_name} — exit code ${exit_code}" + cat "${TEST_TMPDIR}/stdout-${test_name}.log" + FAILURES=$((FAILURES + 1)) + return + fi + + echo "PASS: ${test_name}" +} + +assert_file_contains() { + local test_name="$1" file="$2" pattern="$3" + if ! grep -qF "${pattern}" "${file}"; then + echo "FAIL: ${test_name} — expected '${pattern}' in $(basename "${file}")" + FAILURES=$((FAILURES + 1)) + fi +} + +assert_file_not_contains() { + local test_name="$1" file="$2" pattern="$3" + if grep -qF "${pattern}" "${file}"; then + echo "FAIL: ${test_name} — unexpected '${pattern}' in $(basename "${file}")" + FAILURES=$((FAILURES + 1)) + fi +} + +# --- Tests --- + +# 1. Transcript redaction +run_test "transcript-redaction" +SANITIZED_TRANSCRIPT="${TEST_TMPDIR}/output-transcript-redaction/iteration-1/transcript.jsonl" + +# Emails should be redacted +assert_file_not_contains "transcript-emails" "$SANITIZED_TRANSCRIPT" "alice@example.com" +assert_file_not_contains "transcript-emails" "$SANITIZED_TRANSCRIPT" "bob@corp.com" +assert_file_contains "transcript-emails" "$SANITIZED_TRANSCRIPT" "[redacted-email]" + +# Atlassian hosts should be redacted +assert_file_not_contains "transcript-hosts" "$SANITIZED_TRANSCRIPT" "redhat.atlassian.net" +assert_file_contains "transcript-hosts" "$SANITIZED_TRANSCRIPT" "[redacted-host]" + +# Issue context fields should be redacted +assert_file_not_contains "transcript-body" "$SANITIZED_TRANSCRIPT" "Private issue body with secrets" +assert_file_contains "transcript-body" "$SANITIZED_TRANSCRIPT" "[redacted" + +# Internal URLs should be redacted +assert_file_not_contains "transcript-slack" "$SANITIZED_TRANSCRIPT" "redhat-internal.slack.com" +assert_file_not_contains "transcript-gdocs" "$SANITIZED_TRANSCRIPT" "docs.google.com" + +# Structural metadata should survive +assert_file_contains "transcript-metadata" "$SANITIZED_TRANSCRIPT" "PROJ-123" +assert_file_contains "transcript-metadata" "$SANITIZED_TRANSCRIPT" "Add login" + +# 2. agent-result.json redaction (text scrub only) +SANITIZED_RESULT="${TEST_TMPDIR}/output-transcript-redaction/iteration-1/output/agent-result.json" + +assert_file_not_contains "result-emails" "$SANITIZED_RESULT" "dev@company.com" +assert_file_not_contains "result-hosts" "$SANITIZED_RESULT" "staging.atlassian.net" +assert_file_not_contains "result-slack" "$SANITIZED_RESULT" "redhat-internal.slack.com" +assert_file_contains "result-structure" "$SANITIZED_RESULT" "confidence" +assert_file_contains "result-structure" "$SANITIZED_RESULT" "overall" + +# 3. Missing arguments should fail +if bash "${SANITIZE_SCRIPT}" 2>/dev/null; then + echo "FAIL: missing-args — expected failure" + FAILURES=$((FAILURES + 1)) +else + echo "PASS: missing-args (expected failure)" +fi + +if [[ ${FAILURES} -gt 0 ]]; then + echo "" + echo "${FAILURES} test(s) failed." + exit 1 +fi + +echo "" +echo "All sanitize-artifacts tests passed." diff --git a/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts.sh b/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts.sh new file mode 100755 index 000000000..32bf73593 --- /dev/null +++ b/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +# sanitize-artifacts.sh — Redact private data from agent artifacts before upload. +# +# Keeps transcript structure intact for debugging/tracing while scrubbing: +# - Jira issue descriptions, comment bodies, reporter emails +# - Internal hostnames (*.atlassian.net) +# - Email addresses +# - Parent/linked issue descriptions +# - Internal URLs (Slack, Google Docs, etc.) +# +# What survives: +# - Agent reasoning and tool call commands +# - Issue keys, summaries, statuses, labels (structural metadata) +# - Phase markers and timing +# - agent-result.json (the agent's synthesis, not raw input) +# - exploration_context.json +# +# Usage: sanitize-artifacts.sh + +set -euo pipefail + +OUTPUT_DIR="${1:?Usage: sanitize-artifacts.sh }" +SANITIZED_DIR="${2:?Usage: sanitize-artifacts.sh }" + +mkdir -p "$SANITIZED_DIR" + +python3 - "$OUTPUT_DIR" "$SANITIZED_DIR" <<'PYTHON_SCRIPT' +import json +import os +import re +import sys + +output_dir = sys.argv[1] +sanitized_dir = sys.argv[2] + +EMAIL_RE = re.compile(r'[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}') +ATLASSIAN_HOST_RE = re.compile(r'[a-zA-Z0-9\-]+\.atlassian\.net') +INTERNAL_URL_RE = re.compile(r'https?://redhat-internal\.slack\.com/\S+') +GDOCS_URL_RE = re.compile(r'https?://docs\.google\.com/\S+') + +REDACTED_EMAIL = '[redacted-email]' +REDACTED_HOST = '[redacted-host]' +REDACTED_BODY = '[redacted — private issue content]' +REDACTED_URL = '[redacted-internal-url]' + +def looks_like_issue_context(obj): + if not isinstance(obj, dict): + return False + return ('source' in obj and 'key' in obj + and ('description' in obj or 'summary' in obj) + and obj.get('source') in ('jira', 'github')) + +def redact_issue_context(obj): + obj = dict(obj) + + for field in ('description', 'body'): + if field in obj and obj[field]: + obj[field] = REDACTED_BODY + + if 'reporter' in obj: + obj['reporter'] = REDACTED_EMAIL + + if 'host' in obj: + obj['host'] = REDACTED_HOST + + if 'parent' in obj and isinstance(obj['parent'], dict): + parent = dict(obj['parent']) + for field in ('description', 'body'): + if field in parent: + parent[field] = REDACTED_BODY + obj['parent'] = parent + + if 'comments' in obj and isinstance(obj['comments'], list): + obj['comments'] = [ + {**c, 'body': REDACTED_BODY, 'author': REDACTED_EMAIL} + if isinstance(c, dict) else c + for c in obj['comments'] + ] + + if 'linked_issues' in obj and isinstance(obj['linked_issues'], list): + redacted = [] + for li in obj['linked_issues']: + if isinstance(li, dict): + li = {**li} + if 'description' in li: + li['description'] = REDACTED_BODY + redacted.append(li) + obj['linked_issues'] = redacted + + return obj + +def scrub_text(text): + text = EMAIL_RE.sub(REDACTED_EMAIL, text) + text = ATLASSIAN_HOST_RE.sub(REDACTED_HOST, text) + text = INTERNAL_URL_RE.sub(REDACTED_URL, text) + text = GDOCS_URL_RE.sub(REDACTED_URL, text) + return text + +def find_json_object(text, start=0): + """Find the next top-level JSON object in text starting from start.""" + brace_start = text.find('{', start) + if brace_start == -1: + return None, None, None + + depth = 0 + in_string = False + escape_next = False + + for i in range(brace_start, len(text)): + c = text[i] + if escape_next: + escape_next = False + continue + if c == '\\' and in_string: + escape_next = True + continue + if c == '"' and not escape_next: + in_string = not in_string + continue + if in_string: + continue + if c == '{': + depth += 1 + elif c == '}': + depth -= 1 + if depth == 0: + return brace_start, i + 1, text[brace_start:i + 1] + + return None, None, None + +def redact_content(content_str): + """Redact issue-context JSON blocks from tool output (may be mixed content).""" + if not isinstance(content_str, str): + return content_str + + result_parts = [] + pos = 0 + found_any = False + + while pos < len(content_str): + start, end, json_str = find_json_object(content_str, pos) + + if start is None: + result_parts.append(scrub_text(content_str[pos:])) + break + + result_parts.append(scrub_text(content_str[pos:start])) + + try: + parsed = json.loads(json_str) + if looks_like_issue_context(parsed): + redacted = redact_issue_context(parsed) + result_parts.append(json.dumps(redacted, indent=2)) + found_any = True + else: + result_parts.append(scrub_text(json_str)) + except (json.JSONDecodeError, ValueError): + result_parts.append(scrub_text(json_str)) + + pos = end + + return ''.join(result_parts) + +def redact_jsonl_line(line_str): + try: + obj = json.loads(line_str) + except (json.JSONDecodeError, ValueError): + return scrub_text(line_str) + + msg = obj.get('message', {}) + contents = msg.get('content', []) + + if isinstance(contents, list): + for item in contents: + if not isinstance(item, dict): + continue + if item.get('type') == 'tool_result': + content = item.get('content', '') + if isinstance(content, str): + item['content'] = redact_content(content) + if item.get('type') == 'text': + text = item.get('text', '') + if isinstance(text, str): + item['text'] = scrub_text(text) + if item.get('type') == 'thinking': + thinking = item.get('thinking', '') + if isinstance(thinking, str): + item['thinking'] = scrub_text(thinking) + if item.get('type') == 'tool_use': + inp = item.get('input', {}) + if isinstance(inp, dict): + for k, v in inp.items(): + if isinstance(v, str): + inp[k] = scrub_text(v) + + tool_result = obj.get('toolUseResult', {}) + if isinstance(tool_result, dict): + for field in ('stdout', 'stderr', 'content'): + val = tool_result.get(field, '') + if isinstance(val, str) and val: + tool_result[field] = redact_content(val) + + return json.dumps(obj, ensure_ascii=False) + +transcript_count = 0 +result_count = 0 + +for root, dirs, files in os.walk(output_dir): + for fname in files: + src = os.path.join(root, fname) + rel = os.path.relpath(src, output_dir) + dest = os.path.join(sanitized_dir, rel) + os.makedirs(os.path.dirname(dest), exist_ok=True) + + if fname.endswith('.jsonl'): + with open(src, 'r') as f_in, open(dest, 'w') as f_out: + for line in f_in: + line = line.rstrip('\n') + if line: + f_out.write(redact_jsonl_line(line) + '\n') + transcript_count += 1 + + elif fname == 'agent-result.json': + with open(src, 'r') as f_in, open(dest, 'w') as f_out: + content = f_in.read() + f_out.write(scrub_text(content)) + result_count += 1 + + # Skip sandbox logs (openshell-gateway.log, openshell-sandbox.log) + +print(f'Sanitized {transcript_count} transcript(s), {result_count} result(s)') +print(f'Skipped sandbox logs (gateway/sandbox .log files)') +PYTHON_SCRIPT + +echo "Sanitize complete. Contents:" +find "$SANITIZED_DIR" -type f | sort diff --git a/internal/scaffold/fullsend-repo/skills/jira-read/SKILL.md b/internal/scaffold/fullsend-repo/skills/jira-read/SKILL.md new file mode 100644 index 000000000..b102ebd04 --- /dev/null +++ b/internal/scaffold/fullsend-repo/skills/jira-read/SKILL.md @@ -0,0 +1,106 @@ +--- +name: jira-read +description: >- + Read-only Jira integration skill. Provides patterns for reading issue data, + comments, hierarchy, and linked issues from Jira Cloud instances. + Write operations are handled by post-scripts, not the agent. +--- + +# Jira Read Skill + +This skill provides patterns for reading data from Jira Cloud instances. +All Jira API calls require authentication — the pre-script fetches data +and writes it to files the agent can read. This skill documents what +data is available and how to interpret it. + +## Data available to the agent + +The pre-script fetches issue data and writes it to `$ISSUE_CONTEXT`: + +```json +{ + "source": "jira", + "host": "stage-redhat.atlassian.net", + "key": "SECURESIGN-1620", + "level": "feature", + "summary": "Issue summary text", + "description": "Full issue description (Atlassian Document Format converted to text)", + "status": "In Progress", + "priority": "High", + "labels": ["label1", "label2"], + "reporter": "user@example.com", + "assignee": "user@example.com", + "created": "2026-01-15T10:00:00.000+0000", + "updated": "2026-05-18T14:30:00.000+0000", + "parent": { + "key": "HATSTRAT-259", + "summary": "Parent issue summary", + "description": "Parent description (provides strategic context)" + }, + "children": [ + { + "key": "SECURESIGN-1621", + "summary": "Child issue summary", + "status": "To Do", + "type": "Epic" + } + ], + "comments": [ + { + "author": "user@example.com", + "created": "2026-05-18T15:00:00.000+0000", + "body": "Comment text" + } + ], + "linked_issues": [ + { + "type": "blocks", + "key": "OTHER-123", + "summary": "Linked issue summary", + "status": "Open" + } + ], + "project": { + "key": "SECURESIGN", + "name": "Secure Sign", + "hierarchy": ["Outcome", "Feature", "Epic", "Story", "Task"] + } +} +``` + +## Interpreting issue levels + +The `level` field is derived from the Jira issue type and project hierarchy: + +| Jira type | Level | Decomposes into | +|-----------|-------|----------------| +| Outcome | outcome | features | +| Feature | feature | epics | +| Epic | epic | stories | +| Story | story | tasks | +| Task | task | sub-tasks | +| Bug/Spike | issue | sub-issues | + +## Parent context + +The `parent` field contains the parent issue's description. This often +provides strategic context that the issue itself lacks. Always read the +parent description when available — it may contain goals, constraints, +or requirements not repeated in the child. + +## Comments as conversation history + +The `comments` array contains the full comment history. When the refine +agent previously posted a clarification question and the user answered: + +1. The question will be in an older comment (posted by the bot) +2. The answer will be in a newer comment (posted by a human) + +Check for this pattern to continue iteration without re-asking. + +## What the agent CANNOT do + +- The agent cannot call the Jira API directly (sandbox network policy blocks it) +- The agent cannot create, update, or comment on Jira issues +- All write operations happen in the post-script using credentials that + never enter the sandbox diff --git a/internal/scaffold/fullsend-repo/skills/public-research/SKILL.md b/internal/scaffold/fullsend-repo/skills/public-research/SKILL.md new file mode 100644 index 000000000..31a1b676d --- /dev/null +++ b/internal/scaffold/fullsend-repo/skills/public-research/SKILL.md @@ -0,0 +1,97 @@ +--- +name: public-research +description: >- + Public data research skill. Provides patterns for gathering context from + GitHub, web search, and public documentation. Replaces the internal + org-research skill with public-only data sources. +--- + +# Public Research Skill + +This skill provides techniques for gathering technical context from public +data sources. It replaces the internal `analyze` tool with open, accessible +alternatives. + +## Available data sources + +### 1. GitHub API + +Search issues, PRs, discussions, and code across repositories: + +```bash +# Search issues by keyword +gh issue list --repo OWNER/REPO --state all --search "keywords" \ + --json number,title,state,labels,body --limit 30 + +# Search PRs +gh pr list --repo OWNER/REPO --state all --search "keywords" \ + --json number,title,state,body --limit 20 + +# Search code across GitHub +gh search code "pattern" --repo OWNER/REPO --json path,repository + +# Read file contents +gh api "repos/OWNER/REPO/contents/path/to/file" --jq '.content' | base64 -d + +# List repository topics/languages +gh api "repos/OWNER/REPO" --jq '{topics: .topics, language: .language}' +gh api "repos/OWNER/REPO/languages" +``` + +### 2. Repository analysis + +When the target repo is checked out locally: + +```bash +# Project structure +find . -maxdepth 3 -type f -name "*.go" -o -name "*.py" -o -name "*.ts" | head -50 +tree -L 2 --dirsfirst + +# Dependency manifests +cat go.mod 2>/dev/null || cat package.json 2>/dev/null || cat requirements.txt 2>/dev/null + +# Deployment configs +find . -name "Dockerfile*" -o -name "*.yaml" -path "*/deploy/*" -o -name "Makefile" | head -20 + +# Test infrastructure +find . -name "*_test.go" -o -name "*.test.ts" -o -name "test_*.py" | head -20 +``` + +### 3. Web search + +For competitive analysis and industry standards. Use targeted searches: + +```bash +# Technical documentation +curl -s "https://api.tavily.com/search" \ + -H "Content-Type: application/json" \ + -d '{"query": "specific technical question", "max_results": 5}' +``` + +If Tavily is unavailable, use GitHub as a proxy for public knowledge: + +```bash +gh search repos "topic keywords" --json fullName,description,stargazersCount --limit 10 +``` + +### 4. Public documentation + +Read README files and docs from related repositories: + +```bash +gh api "repos/OWNER/REPO/readme" --jq '.content' | base64 -d +``` + +## Research strategy + +1. **Start with the target repo** — understand what exists before searching externally +2. **Search for related work** — prior issues, PRs, and discussions +3. **Search related repos** — projects in the same org or ecosystem +4. **Web search last** — only for gaps not covered by repo/GitHub analysis + +## What NOT to do + +- Do not access internal/proprietary tools or databases +- Do not fabricate sources — if you can't find information, note the gap +- Do not do unfocused research — every search should answer a specific question +- Do not spend excessive tokens on broad web crawling diff --git a/internal/scaffold/render.go b/internal/scaffold/render.go index d22644dc1..402c9a3d7 100644 --- a/internal/scaffold/render.go +++ b/internal/scaffold/render.go @@ -31,6 +31,10 @@ var thinStageWorkflows = []struct { {"fix", ".github/workflows/fix.yml"}, {"retro", ".github/workflows/retro.yml"}, {"prioritize", ".github/workflows/prioritize.yml"}, + {"explore", ".github/workflows/explore.yml"}, + {"refine", ".github/workflows/refine.yml"}, + {"critique", ".github/workflows/critique.yml"}, + {"create-children", ".github/workflows/create-children.yml"}, } // RenderTemplate applies vendoring-aware substitutions to scaffold templates. diff --git a/internal/scaffold/scaffold.go b/internal/scaffold/scaffold.go index dbd44f643..be1a756a1 100644 --- a/internal/scaffold/scaffold.go +++ b/internal/scaffold/scaffold.go @@ -42,6 +42,22 @@ var executableFiles = map[string]struct{}{ "scripts/fullsend-check-output": {}, "scripts/validate-output-schema-test.sh": {}, "scripts/validate-source-repo.sh": {}, + "scripts/pre-explore.sh": {}, + "scripts/post-explore.sh": {}, + "scripts/pre-refine.sh": {}, + "scripts/post-refine.sh": {}, + "scripts/pre-critique.sh": {}, + "scripts/post-critique.sh": {}, + "scripts/create-children.sh": {}, + "scripts/pipeline-events.sh": {}, + "scripts/markdown-to-adf.py": {}, + "scripts/pipeline-helpers.sh": {}, + "scripts/sanitize-artifacts.sh": {}, + "scripts/post-explore-test.sh": {}, + "scripts/post-refine-test.sh": {}, + "scripts/post-critique-test.sh": {}, + "scripts/create-children-test.sh": {}, + "scripts/sanitize-artifacts-test.sh": {}, } // FileMode returns the Git tree mode for a scaffold file. diff --git a/internal/scaffold/scaffold_test.go b/internal/scaffold/scaffold_test.go index 0ca8f6c0d..e88ee47fb 100644 --- a/internal/scaffold/scaffold_test.go +++ b/internal/scaffold/scaffold_test.go @@ -94,6 +94,39 @@ func TestFullsendRepoFilesExist(t *testing.T) { "scripts/post-prioritize-test.sh", ".github/workflows/prioritize.yml", ".github/workflows/prioritize-scheduler.yml", + // Refinement pipeline + "agents/explore.md", + "agents/refine.md", + "agents/critique.md", + "harness/explore.yaml", + "harness/refine.yaml", + "harness/critique.yaml", + "policies/explore.yaml", + "policies/refine.yaml", + "policies/critique.yaml", + "schemas/explore-result.schema.json", + "schemas/refine-result.schema.json", + "schemas/critique-result.schema.json", + "scripts/pre-explore.sh", + "scripts/post-explore.sh", + "scripts/pre-refine.sh", + "scripts/post-refine.sh", + "scripts/pre-critique.sh", + "scripts/post-critique.sh", + "scripts/create-children.sh", + "scripts/pipeline-events.sh", + "scripts/pipeline-helpers.sh", + "scripts/markdown-to-adf.py", + "scripts/sanitize-artifacts.sh", + "skills/jira-read/SKILL.md", + "skills/public-research/SKILL.md", + ".github/workflows/explore.yml", + ".github/workflows/refine.yml", + ".github/workflows/critique.yml", + ".github/workflows/create-children.yml", + ".github/workflows/refine-dispatch.yml", + ".github/workflows/jira-dispatch.yml", + ".github/workflows/jira-comment-poller.yml", } for _, path := range expected { @@ -882,6 +915,336 @@ func TestPrioritizeHarnessContent(t *testing.T) { assert.Contains(t, s, "PROJECT_NUMBER") } +// --- Refinement pipeline content tests --- + +func TestExploreWorkflowContent(t *testing.T) { + content, err := FullsendRepoFile(".github/workflows/explore.yml") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "# fullsend-stage: explore") + assert.Contains(t, s, "workflow_dispatch") + assert.Contains(t, s, "issue_key") + assert.Contains(t, s, "issue_source") + assert.Contains(t, s, "__REUSABLE_WORKFLOW__") + assert.Contains(t, s, "FULLSEND_MINT_URL") + assert.NotContains(t, s, "secrets: inherit") + assert.Contains(t, s, "FULLSEND_GCP_WIF_PROVIDER: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }}") + assert.Contains(t, s, "FULLSEND_GCP_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }}") + assert.Contains(t, s, "concurrency:") + assert.Contains(t, s, "fullsend-explore-") + assert.Contains(t, s, "cancel-in-progress: true") + assert.Contains(t, s, "permissions:") + assert.Contains(t, s, "actions: write") + assert.Contains(t, s, "id-token: write") + assert.Contains(t, s, "issues: write") + assert.Contains(t, s, "contents: read") + assert.Contains(t, s, "JIRA_HOST") + assert.Contains(t, s, "JIRA_EMAIL") + assert.Contains(t, s, "JIRA_API_TOKEN") + assert.Contains(t, s, "jira_project_visibility") +} + +func TestRefineWorkflowContent(t *testing.T) { + content, err := FullsendRepoFile(".github/workflows/refine.yml") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "# fullsend-stage: refine") + assert.Contains(t, s, "workflow_dispatch") + assert.Contains(t, s, "issue_key") + assert.Contains(t, s, "explore_run_id") + assert.Contains(t, s, "review_round") + assert.Contains(t, s, "max_review_rounds") + assert.Contains(t, s, "auto_create") + assert.Contains(t, s, "critique_run_id") + assert.Contains(t, s, "__REUSABLE_WORKFLOW__") + assert.Contains(t, s, "FULLSEND_MINT_URL") + assert.NotContains(t, s, "secrets: inherit") + assert.Contains(t, s, "FULLSEND_GCP_WIF_PROVIDER: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }}") + assert.Contains(t, s, "FULLSEND_GCP_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }}") + assert.Contains(t, s, "concurrency:") + assert.Contains(t, s, "fullsend-refine-") + assert.Contains(t, s, "cancel-in-progress: false") + assert.Contains(t, s, "permissions:") + assert.Contains(t, s, "actions: write") + assert.Contains(t, s, "id-token: write") + assert.Contains(t, s, "issues: write") + assert.Contains(t, s, "contents: read") +} + +func TestCritiqueWorkflowContent(t *testing.T) { + content, err := FullsendRepoFile(".github/workflows/critique.yml") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "# fullsend-stage: critique") + assert.Contains(t, s, "workflow_dispatch") + assert.Contains(t, s, "issue_key") + assert.Contains(t, s, "refine_run_id") + assert.Contains(t, s, "review_round") + assert.Contains(t, s, "max_review_rounds") + assert.Contains(t, s, "auto_create") + assert.Contains(t, s, "__REUSABLE_WORKFLOW__") + assert.Contains(t, s, "FULLSEND_MINT_URL") + assert.NotContains(t, s, "secrets: inherit") + assert.Contains(t, s, "FULLSEND_GCP_WIF_PROVIDER: ${{ secrets.FULLSEND_GCP_WIF_PROVIDER }}") + assert.Contains(t, s, "FULLSEND_GCP_PROJECT_ID: ${{ secrets.FULLSEND_GCP_PROJECT_ID }}") + assert.Contains(t, s, "concurrency:") + assert.Contains(t, s, "fullsend-critique-") + assert.Contains(t, s, "cancel-in-progress: false") + assert.Contains(t, s, "permissions:") + assert.Contains(t, s, "actions: write") + assert.Contains(t, s, "id-token: write") + assert.Contains(t, s, "issues: write") + assert.Contains(t, s, "contents: read") +} + +func TestCreateChildrenWorkflowContent(t *testing.T) { + content, err := FullsendRepoFile(".github/workflows/create-children.yml") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "# fullsend-stage: create-children") + assert.Contains(t, s, "workflow_dispatch") + assert.Contains(t, s, "issue_key") + assert.Contains(t, s, "refine_run_id") + assert.Contains(t, s, "__REUSABLE_WORKFLOW__") + assert.Contains(t, s, "FULLSEND_MINT_URL") + assert.NotContains(t, s, "secrets: inherit") + assert.Contains(t, s, "concurrency:") + assert.Contains(t, s, "fullsend-create-children-") + assert.Contains(t, s, "cancel-in-progress: false") +} + +func TestRefineDispatchWorkflowContent(t *testing.T) { + content, err := FullsendRepoFile(".github/workflows/refine-dispatch.yml") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "/fs-refine") + assert.Contains(t, s, "/fs-create") + assert.Contains(t, s, "author_association == 'OWNER'") + assert.Contains(t, s, "author_association == 'MEMBER'") + assert.Contains(t, s, "author_association == 'COLLABORATOR'") + assert.Contains(t, s, "refine-approved") + assert.Contains(t, s, "explore.yml") + assert.Contains(t, s, "refine.yml") +} + +func TestJiraDispatchWorkflowContent(t *testing.T) { + content, err := FullsendRepoFile(".github/workflows/jira-dispatch.yml") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "/fs-refine") + assert.Contains(t, s, "jira_key") + assert.Contains(t, s, "JIRA_PROJECT_VISIBILITY") + assert.Contains(t, s, "explore.yml") +} + +func TestJiraCommentPollerWorkflowContent(t *testing.T) { + content, err := FullsendRepoFile(".github/workflows/jira-comment-poller.yml") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "schedule") + assert.Contains(t, s, "cron") + assert.Contains(t, s, "labels = fullsend") + assert.Contains(t, s, "jira-dispatch.yml") +} + +func TestExploreAgentPromptContent(t *testing.T) { + content, err := FullsendRepoFile("agents/explore.md") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "agent-result.json") + assert.Contains(t, s, "confidence") + assert.Contains(t, s, "technical_landscape") + assert.Contains(t, s, "related_work") + assert.Contains(t, s, "disallowedTools") +} + +func TestRefineAgentPromptContent(t *testing.T) { + content, err := FullsendRepoFile("agents/refine.md") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "agent-result.json") + assert.Contains(t, s, "children") + assert.Contains(t, s, "acceptance_criteria") + assert.Contains(t, s, "parent_title") + assert.Contains(t, s, "disallowedTools") +} + +func TestCritiqueAgentPromptContent(t *testing.T) { + content, err := FullsendRepoFile("agents/critique.md") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "agent-result.json") + assert.Contains(t, s, "verdict") + assert.Contains(t, s, "approved") + assert.Contains(t, s, "revise") + assert.Contains(t, s, "needs_input") + assert.Contains(t, s, "disallowedTools") +} + +func TestExploreSchemaContent(t *testing.T) { + content, err := FullsendRepoFile("schemas/explore-result.schema.json") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "$schema") + assert.Contains(t, s, "technical_landscape") + assert.Contains(t, s, "related_work") + assert.Contains(t, s, "confidence") + assert.Contains(t, s, "summary") +} + +func TestRefineSchemaContent(t *testing.T) { + content, err := FullsendRepoFile("schemas/refine-result.schema.json") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "$schema") + assert.Contains(t, s, "children") + assert.Contains(t, s, "acceptance_criteria") + assert.Contains(t, s, "parent_title") + assert.Contains(t, s, "complete") +} + +func TestCritiqueSchemaContent(t *testing.T) { + content, err := FullsendRepoFile("schemas/critique-result.schema.json") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "$schema") + assert.Contains(t, s, "verdict") + assert.Contains(t, s, "approved") + assert.Contains(t, s, "revise") + assert.Contains(t, s, "needs_input") + assert.Contains(t, s, "assessment") +} + +func TestExploreHarnessContent(t *testing.T) { + content, err := FullsendRepoFile("harness/explore.yaml") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "agents/explore.md") + assert.Contains(t, s, "pre_script") + assert.Contains(t, s, "post_script") + assert.Contains(t, s, "runner_env") + assert.Contains(t, s, "ISSUE_KEY") +} + +func TestRefineHarnessContent(t *testing.T) { + content, err := FullsendRepoFile("harness/refine.yaml") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "agents/refine.md") + assert.Contains(t, s, "pre_script") + assert.Contains(t, s, "post_script") + assert.Contains(t, s, "runner_env") + assert.Contains(t, s, "EXPLORE_RUN_ID") +} + +func TestCritiqueHarnessContent(t *testing.T) { + content, err := FullsendRepoFile("harness/critique.yaml") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "agents/critique.md") + assert.Contains(t, s, "pre_script") + assert.Contains(t, s, "post_script") + assert.Contains(t, s, "runner_env") + assert.Contains(t, s, "REFINE_RUN_ID") +} + +func TestHarnessForgeRunnerEnvMergeRefinementPipeline(t *testing.T) { + dir := t.TempDir() + err := WalkFullsendRepoAll(func(path string, content []byte) error { + dest := filepath.Join(dir, path) + if mkErr := os.MkdirAll(filepath.Dir(dest), 0o755); mkErr != nil { + return mkErr + } + return os.WriteFile(dest, content, 0o644) + }) + require.NoError(t, err, "extracting scaffold") + + tests := []struct { + file string + topLevelKeys []string + forgeGithubKeys []string + }{ + { + file: "explore.yaml", + topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA", "ISSUE_KEY"}, + forgeGithubKeys: []string{"GH_TOKEN"}, + }, + { + file: "refine.yaml", + topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA", "ISSUE_KEY", "EXPLORE_RUN_ID"}, + forgeGithubKeys: []string{"GH_TOKEN"}, + }, + { + file: "critique.yaml", + topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA", "ISSUE_KEY", "REFINE_RUN_ID"}, + forgeGithubKeys: []string{"GH_TOKEN"}, + }, + } + + for _, tt := range tests { + t.Run(tt.file, func(t *testing.T) { + harnessPath := filepath.Join(dir, "harness", tt.file) + h, loadErr := harness.LoadWithOpts(harnessPath, harness.LoadOpts{ForgePlatform: "github"}) + require.NoError(t, loadErr) + + for _, key := range tt.topLevelKeys { + assert.Contains(t, h.RunnerEnv, key, "merged RunnerEnv should contain top-level key %s", key) + } + for _, key := range tt.forgeGithubKeys { + assert.Contains(t, h.RunnerEnv, key, "merged RunnerEnv should contain forge.github key %s", key) + } + }) + } +} + +func TestPipelineHelpersContent(t *testing.T) { + content, err := FullsendRepoFile("scripts/pipeline-helpers.sh") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "find_agent_result") + assert.Contains(t, s, "determine_reply_target") + assert.Contains(t, s, "post_comment") + assert.Contains(t, s, "build_run_link") + assert.Contains(t, s, "add_label") + assert.Contains(t, s, "remove_label") + assert.Contains(t, s, "jira_comment") +} + +func TestPipelineEventsContent(t *testing.T) { + content, err := FullsendRepoFile("scripts/pipeline-events.sh") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "pe_start") + assert.Contains(t, s, "pe_end") + assert.Contains(t, s, "pe_error") + assert.Contains(t, s, "pe_copy_to_output") + assert.Contains(t, s, "pipeline-events.jsonl") +} + +func TestSanitizeArtifactsContent(t *testing.T) { + content, err := FullsendRepoFile("scripts/sanitize-artifacts.sh") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "redact") + assert.Contains(t, s, "atlassian.net") + assert.Contains(t, s, "redacted-email") + assert.Contains(t, s, "redacted-host") + assert.Contains(t, s, "agent-result.json") +} + +func TestCreateChildrenScriptContent(t *testing.T) { + content, err := FullsendRepoFile("scripts/create-children.sh") + require.NoError(t, err) + s := string(content) + assert.Contains(t, s, "RESULT_FILE") + assert.Contains(t, s, "github_create_issue") + assert.Contains(t, s, "jira_create_issue") + assert.Contains(t, s, "resolve_jira_type") + assert.Contains(t, s, "TITLE_TO_KEY") + assert.Contains(t, s, "MAX_PASSES=5") + assert.Contains(t, s, "CREATED_CHILD_COUNT") +} + func TestAllScaffoldYAMLDocumentStartMarker(t *testing.T) { // yamllint document-start rule requires --- at the top of every YAML file. // Walk embedded scaffold YAML/YML files and verify each starts with "---\n". diff --git a/internal/scaffold/vendormanifest.go b/internal/scaffold/vendormanifest.go index ccc5f6c8c..93d6532c1 100644 --- a/internal/scaffold/vendormanifest.go +++ b/internal/scaffold/vendormanifest.go @@ -136,9 +136,13 @@ func (m *VendorManifest) CleanupPaths(workflowPrefix string) []string { var vendoredReusableWorkflows = []string{ "reusable-code.yml", + "reusable-create-children.yml", + "reusable-critique.yml", "reusable-dispatch.yml", + "reusable-explore.yml", "reusable-fix.yml", "reusable-prioritize.yml", + "reusable-refine.yml", "reusable-retro.yml", "reusable-review.yml", "reusable-triage.yml", diff --git a/internal/scaffold/workflow_call_alignment_test.go b/internal/scaffold/workflow_call_alignment_test.go index 0379396e7..29d932504 100644 --- a/internal/scaffold/workflow_call_alignment_test.go +++ b/internal/scaffold/workflow_call_alignment_test.go @@ -47,7 +47,7 @@ type callerJob struct { // reusableWorkflowRef extracts the reusable workflow filename from a uses: reference. // Handles both "fullsend-ai/fullsend/.github/workflows/reusable-foo.yml@v0" // and "./.github/workflows/reusable-foo.yml". -var reusableWorkflowRef = regexp.MustCompile(`reusable-[a-z]+\.yml`) +var reusableWorkflowRef = regexp.MustCompile(`reusable-[a-z][-a-z]*\.yml`) // callerPair defines a caller → reusable workflow relationship to validate. type callerPair struct { @@ -97,6 +97,10 @@ func TestWorkflowCallInputAlignment(t *testing.T) { {"scaffold/fix.yml", loadRenderedScaffoldCaller(".github/workflows/fix.yml"), "fix"}, {"scaffold/retro.yml", loadRenderedScaffoldCaller(".github/workflows/retro.yml"), "retro"}, {"scaffold/prioritize.yml", loadRenderedScaffoldCaller(".github/workflows/prioritize.yml"), "prioritize"}, + {"scaffold/explore.yml", loadRenderedScaffoldCaller(".github/workflows/explore.yml"), "explore"}, + {"scaffold/refine.yml", loadRenderedScaffoldCaller(".github/workflows/refine.yml"), "refine"}, + {"scaffold/critique.yml", loadRenderedScaffoldCaller(".github/workflows/critique.yml"), "critique"}, + {"scaffold/create-children.yml", loadRenderedScaffoldCaller(".github/workflows/create-children.yml"), "create-children"}, } // Also validate reusable-dispatch.yml's stage jobs. @@ -184,7 +188,7 @@ func TestReusableWorkflowsShareCommonInputs(t *testing.T) { "FULLSEND_GCP_PROJECT_ID", } - stages := []string{"triage", "code", "review", "fix", "retro", "prioritize"} + stages := []string{"triage", "code", "review", "fix", "retro", "prioritize", "explore", "refine", "critique", "create-children"} for _, stage := range stages { t.Run(stage, func(t *testing.T) { From 4c5a52afe5b97c4707fbe334b8b43338f6108488 Mon Sep 17 00:00:00 2001 From: Adam Scerra Date: Thu, 18 Jun 2026 13:06:24 -0400 Subject: [PATCH 2/8] fix(scaffold): address test review feedback for refinement pipeline - Merge TestHarnessForgeRunnerEnvMergeRefinementPipeline into existing TestHarnessForgeRunnerEnvMerge table to avoid duplicate scaffold extraction - Add Jira secret threading assertions (JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN, jira_project_visibility) to refine/critique content tests - Fix post-explore-test.sh no-op assertion (replace && true with real check) - Add /tmp/workspace cleanup to post-critique-test.sh trap to prevent CI state leakage - Add assert_gh_not_called helpers to explore/refine test scripts - Add invalid JSON test case to post-refine-test.sh Co-authored-by: Cursor Signed-off-by: Adam Scerra --- .../scripts/post-critique-test.sh | 2 +- .../scripts/post-explore-test.sh | 15 ++-- .../fullsend-repo/scripts/post-refine-test.sh | 12 ++++ internal/scaffold/scaffold_test.go | 72 ++++++------------- 4 files changed, 44 insertions(+), 57 deletions(-) diff --git a/internal/scaffold/fullsend-repo/scripts/post-critique-test.sh b/internal/scaffold/fullsend-repo/scripts/post-critique-test.sh index bdccc94b3..80370614f 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-critique-test.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-critique-test.sh @@ -10,7 +10,7 @@ POST_SCRIPT="${SCRIPT_DIR}/post-critique.sh" FAILURES=0 TEST_TMPDIR="$(mktemp -d)" -trap 'rm -rf "${TEST_TMPDIR}"' EXIT +trap 'rm -rf "${TEST_TMPDIR}" /tmp/workspace/critique-history.json /tmp/workspace/critique-feedback.json /tmp/workspace/refine-result.json' EXIT GH_LOG="${TEST_TMPDIR}/gh-calls.log" MOCK_BIN="${TEST_TMPDIR}/bin" diff --git a/internal/scaffold/fullsend-repo/scripts/post-explore-test.sh b/internal/scaffold/fullsend-repo/scripts/post-explore-test.sh index efce79e0e..1c5fe17d4 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-explore-test.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-explore-test.sh @@ -114,10 +114,11 @@ assert_gh_called() { fi } -assert_file_exists() { - local test_name="$1" path="$2" - if [[ ! -f "${path}" ]]; then - echo "FAIL: ${test_name} — expected file at ${path}" +assert_gh_not_called() { + local test_name="$1" pattern="$2" + if grep -qF "${pattern}" "${GH_LOG}"; then + echo "FAIL: ${test_name} — unexpected gh call matching '${pattern}'" + cat "${GH_LOG}" FAILURES=$((FAILURES + 1)) fi } @@ -129,12 +130,12 @@ run_test "happy-path" assert_gh_called "happy-path" "workflow run refine.yml" assert_gh_called "happy-path" "issue_key=42" assert_gh_called "happy-path" "explore_run_id=12345" -assert_file_exists "happy-path" "${TEST_TMPDIR}/run-happy-path/../../workspace/exploration_context.json" && true -# Check /tmp/workspace/ was created (the script writes there) if [[ -f "/tmp/workspace/exploration_context.json" ]]; then echo "PASS: happy-path exploration_context.json saved" + rm -f "/tmp/workspace/exploration_context.json" else - echo "INFO: happy-path — /tmp/workspace not checked (may require cleanup)" + echo "FAIL: happy-path — exploration_context.json not saved to /tmp/workspace/" + FAILURES=$((FAILURES + 1)) fi # Auto-create propagation diff --git a/internal/scaffold/fullsend-repo/scripts/post-refine-test.sh b/internal/scaffold/fullsend-repo/scripts/post-refine-test.sh index af997485e..2798e636b 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-refine-test.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-refine-test.sh @@ -124,6 +124,15 @@ assert_stdout_contains() { fi } +assert_gh_not_called() { + local test_name="$1" pattern="$2" + if grep -qF "${pattern}" "${GH_LOG}"; then + echo "FAIL: ${test_name} — unexpected gh call matching '${pattern}'" + cat "${GH_LOG}" + FAILURES=$((FAILURES + 1)) + fi +} + # --- Tests --- # Happy path: refine completes and chains critique @@ -162,6 +171,9 @@ else FAILURES=$((FAILURES + 1)) fi +# Invalid JSON result +run_test "invalid-json" "not valid json" "true" + if [[ ${FAILURES} -gt 0 ]]; then echo "" echo "${FAILURES} test(s) failed." diff --git a/internal/scaffold/scaffold_test.go b/internal/scaffold/scaffold_test.go index e88ee47fb..e2cc37f1a 100644 --- a/internal/scaffold/scaffold_test.go +++ b/internal/scaffold/scaffold_test.go @@ -706,6 +706,21 @@ func TestHarnessForgeRunnerEnvMerge(t *testing.T) { topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA"}, forgeGithubKeys: []string{"GITHUB_ISSUE_URL", "GH_TOKEN", "ORG", "PROJECT_NUMBER"}, }, + { + file: "explore.yaml", + topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA", "ISSUE_KEY"}, + forgeGithubKeys: []string{"GH_TOKEN"}, + }, + { + file: "refine.yaml", + topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA", "ISSUE_KEY", "EXPLORE_RUN_ID"}, + forgeGithubKeys: []string{"GH_TOKEN"}, + }, + { + file: "critique.yaml", + topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA", "ISSUE_KEY", "REFINE_RUN_ID"}, + forgeGithubKeys: []string{"GH_TOKEN"}, + }, } for _, tt := range tests { @@ -969,6 +984,10 @@ func TestRefineWorkflowContent(t *testing.T) { assert.Contains(t, s, "id-token: write") assert.Contains(t, s, "issues: write") assert.Contains(t, s, "contents: read") + assert.Contains(t, s, "JIRA_HOST") + assert.Contains(t, s, "JIRA_EMAIL") + assert.Contains(t, s, "JIRA_API_TOKEN") + assert.Contains(t, s, "jira_project_visibility") } func TestCritiqueWorkflowContent(t *testing.T) { @@ -995,6 +1014,10 @@ func TestCritiqueWorkflowContent(t *testing.T) { assert.Contains(t, s, "id-token: write") assert.Contains(t, s, "issues: write") assert.Contains(t, s, "contents: read") + assert.Contains(t, s, "JIRA_HOST") + assert.Contains(t, s, "JIRA_EMAIL") + assert.Contains(t, s, "JIRA_API_TOKEN") + assert.Contains(t, s, "jira_project_visibility") } func TestCreateChildrenWorkflowContent(t *testing.T) { @@ -1148,55 +1171,6 @@ func TestCritiqueHarnessContent(t *testing.T) { assert.Contains(t, s, "REFINE_RUN_ID") } -func TestHarnessForgeRunnerEnvMergeRefinementPipeline(t *testing.T) { - dir := t.TempDir() - err := WalkFullsendRepoAll(func(path string, content []byte) error { - dest := filepath.Join(dir, path) - if mkErr := os.MkdirAll(filepath.Dir(dest), 0o755); mkErr != nil { - return mkErr - } - return os.WriteFile(dest, content, 0o644) - }) - require.NoError(t, err, "extracting scaffold") - - tests := []struct { - file string - topLevelKeys []string - forgeGithubKeys []string - }{ - { - file: "explore.yaml", - topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA", "ISSUE_KEY"}, - forgeGithubKeys: []string{"GH_TOKEN"}, - }, - { - file: "refine.yaml", - topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA", "ISSUE_KEY", "EXPLORE_RUN_ID"}, - forgeGithubKeys: []string{"GH_TOKEN"}, - }, - { - file: "critique.yaml", - topLevelKeys: []string{"FULLSEND_OUTPUT_SCHEMA", "ISSUE_KEY", "REFINE_RUN_ID"}, - forgeGithubKeys: []string{"GH_TOKEN"}, - }, - } - - for _, tt := range tests { - t.Run(tt.file, func(t *testing.T) { - harnessPath := filepath.Join(dir, "harness", tt.file) - h, loadErr := harness.LoadWithOpts(harnessPath, harness.LoadOpts{ForgePlatform: "github"}) - require.NoError(t, loadErr) - - for _, key := range tt.topLevelKeys { - assert.Contains(t, h.RunnerEnv, key, "merged RunnerEnv should contain top-level key %s", key) - } - for _, key := range tt.forgeGithubKeys { - assert.Contains(t, h.RunnerEnv, key, "merged RunnerEnv should contain forge.github key %s", key) - } - }) - } -} - func TestPipelineHelpersContent(t *testing.T) { content, err := FullsendRepoFile("scripts/pipeline-helpers.sh") require.NoError(t, err) From 3ff50566328b6dd7b407c8d1b098674823599353 Mon Sep 17 00:00:00 2001 From: Adam Scerra Date: Thu, 18 Jun 2026 13:20:35 -0400 Subject: [PATCH 3/8] fix: address CI lint failures and least-privilege permissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create-children: actions:write → actions:read (terminal step, no dispatch) - Renumber ADRs: 0049→0051, 0050→0052 (avoid conflicts with other branches) - Add required YAML frontmatter to both ADRs - Add missing agent doc sections (Control labels, Configuration and extension) - Add OPTIONAL_SECTIONS to hack/lint-agent-docs for pipeline-specific headings - Add placeholder agent icons (explore, refine, critique) - Fix ruff lint/format violations in markdown-to-adf.py - Fix shellcheck SC2034 (HAS_PE) and SC2129 (redirect grouping) - Fix trailing newlines in policy YAML files - Add header comments to reusable workflows matching existing convention Co-authored-by: Cursor Signed-off-by: Adam Scerra --- .../workflows/reusable-create-children.yml | 4 +- .github/workflows/reusable-critique.yml | 2 + .github/workflows/reusable-explore.yml | 2 + .github/workflows/reusable-refine.yml | 2 + ... 0051-refinement-pipeline-architecture.md} | 15 +- ...odel.md => 0052-jira-integration-model.md} | 13 +- docs/agents/critique.md | 13 ++ docs/agents/explore.md | 12 ++ docs/agents/icons/critique.png | Bin 0 -> 77256 bytes docs/agents/icons/explore.png | Bin 0 -> 103701 bytes docs/agents/icons/refine.png | Bin 0 -> 62993 bytes docs/agents/refine.md | 12 ++ hack/lint-agent-docs | 12 +- .../.github/workflows/create-children.yml | 2 +- .../fullsend-repo/policies/critique.yaml | 1 - .../fullsend-repo/policies/refine.yaml | 1 - .../fullsend-repo/scripts/create-children.sh | 2 + .../fullsend-repo/scripts/markdown-to-adf.py | 173 +++++++++++------- .../fullsend-repo/scripts/pre-critique.sh | 14 +- .../fullsend-repo/scripts/pre-refine.sh | 8 +- 20 files changed, 209 insertions(+), 79 deletions(-) rename docs/ADRs/{0049-refinement-pipeline-architecture.md => 0051-refinement-pipeline-architecture.md} (96%) rename docs/ADRs/{0050-jira-integration-model.md => 0052-jira-integration-model.md} (97%) create mode 100644 docs/agents/icons/critique.png create mode 100644 docs/agents/icons/explore.png create mode 100644 docs/agents/icons/refine.png diff --git a/.github/workflows/reusable-create-children.yml b/.github/workflows/reusable-create-children.yml index 224bf1c68..e59518f5a 100644 --- a/.github/workflows/reusable-create-children.yml +++ b/.github/workflows/reusable-create-children.yml @@ -1,3 +1,5 @@ +# Reusable create-children workflow. Called after critique approves a refinement. +# Creates child issues in GitHub and/or Jira from the refine agent's output. name: Create Children on: @@ -58,7 +60,7 @@ jobs: name: Create Children runs-on: ubuntu-latest permissions: - actions: write + actions: read contents: read id-token: write issues: write diff --git a/.github/workflows/reusable-critique.yml b/.github/workflows/reusable-critique.yml index 76055c932..68e21351b 100644 --- a/.github/workflows/reusable-critique.yml +++ b/.github/workflows/reusable-critique.yml @@ -1,3 +1,5 @@ +# Reusable critique agent workflow. Called by thin callers in .fullsend repos +# via workflow_call. Runs in the caller's repo context (secrets, checkout). name: Critique Agent on: diff --git a/.github/workflows/reusable-explore.yml b/.github/workflows/reusable-explore.yml index 633c37b5f..20f17430e 100644 --- a/.github/workflows/reusable-explore.yml +++ b/.github/workflows/reusable-explore.yml @@ -1,3 +1,5 @@ +# Reusable explore agent workflow. Called by thin callers in .fullsend repos +# via workflow_call. Runs in the caller's repo context (secrets, checkout). name: Explore Agent on: diff --git a/.github/workflows/reusable-refine.yml b/.github/workflows/reusable-refine.yml index f453db03b..d242cee61 100644 --- a/.github/workflows/reusable-refine.yml +++ b/.github/workflows/reusable-refine.yml @@ -1,3 +1,5 @@ +# Reusable refine agent workflow. Called by thin callers in .fullsend repos +# via workflow_call. Runs in the caller's repo context (secrets, checkout). name: Refine Agent on: diff --git a/docs/ADRs/0049-refinement-pipeline-architecture.md b/docs/ADRs/0051-refinement-pipeline-architecture.md similarity index 96% rename from docs/ADRs/0049-refinement-pipeline-architecture.md rename to docs/ADRs/0051-refinement-pipeline-architecture.md index 9beb8cb18..f37843dd0 100644 --- a/docs/ADRs/0049-refinement-pipeline-architecture.md +++ b/docs/ADRs/0051-refinement-pipeline-architecture.md @@ -1,4 +1,15 @@ -# ADR 0049: Refinement Pipeline Architecture +--- +title: "51. Refinement Pipeline Architecture" +status: Accepted +relates_to: [] +topics: + - agents + - refinement + - pipeline + - multi-agent orchestration +--- + +# ADR 0051: Refinement Pipeline Architecture ## Status @@ -83,4 +94,4 @@ This is a **Phase 1 implementation** focused on getting the pipeline functional - The revision loop could produce unnecessary iterations on simple issues (mitigated by confidence scoring and max rounds) - Artifact downloads between workflow runs add latency (~30s per stage transition) -- Private Jira data could leak via public repo artifacts/logs (mitigated by a safety check that blocks Jira sources on public repos — see ADR 0050) +- Private Jira data could leak via public repo artifacts/logs (mitigated by a safety check that blocks Jira sources on public repos — see ADR 0052) diff --git a/docs/ADRs/0050-jira-integration-model.md b/docs/ADRs/0052-jira-integration-model.md similarity index 97% rename from docs/ADRs/0050-jira-integration-model.md rename to docs/ADRs/0052-jira-integration-model.md index ee4cd55eb..0bf3d9016 100644 --- a/docs/ADRs/0050-jira-integration-model.md +++ b/docs/ADRs/0052-jira-integration-model.md @@ -1,4 +1,15 @@ -# ADR 0050: Jira Integration Model +--- +title: "52. Jira Integration Model" +status: Accepted +relates_to: [] +topics: + - jira + - integration + - issue tracking + - credentials +--- + +# ADR 0052: Jira Integration Model ## Status diff --git a/docs/agents/critique.md b/docs/agents/critique.md index e4de1463f..13c323059 100644 --- a/docs/agents/critique.md +++ b/docs/agents/critique.md @@ -21,6 +21,19 @@ Nothing gets created until this agent approves. - Guards against scope creep — children that exceed what the parent asked for. - Catches "assumption laundering" — plans that look implementable but are built on unverified guesses. +## Control labels + +| Label | Meaning | +|-------|---------| +| `fullsend:critiquing` | Applied by the critique pre-script while evaluation is in progress. Removed on completion. | +| `fullsend:approved` | Applied by the critique post-script when the plan receives an `approved` verdict. | +| `fullsend:needs-input` | Applied by the critique post-script when human input is required before the pipeline can proceed. | + +## Configuration and extension + +See [Customizing with AGENTS.md](../guides/user/customizing-with-agents-md.md) and +[Customizing with Skills](../guides/user/customizing-with-skills.md). + ## Verdicts | Verdict | Meaning | What happens next | diff --git a/docs/agents/explore.md b/docs/agents/explore.md index 410032c1c..df0178bd5 100644 --- a/docs/agents/explore.md +++ b/docs/agents/explore.md @@ -31,6 +31,18 @@ For Jira issues, the pipeline can also be triggered by: - A Jira Automation webhook sending a `repository_dispatch` event - The Jira comment poller (cron job, every 5 minutes) detecting `/fs-refine` comments +## Control labels + +| Label | Meaning | +|-------|---------| +| `fullsend:exploring` | Applied by the explore pre-script while exploration is in progress. Removed on completion. | +| `fullsend:explored` | Applied by the explore post-script when exploration completes successfully. Signals that refine can proceed. | + +## Configuration and extension + +See [Customizing with AGENTS.md](../guides/user/customizing-with-agents-md.md) and +[Customizing with Skills](../guides/user/customizing-with-skills.md). + ## Pipeline flow ``` diff --git a/docs/agents/icons/critique.png b/docs/agents/icons/critique.png new file mode 100644 index 0000000000000000000000000000000000000000..5ea8aff25158f74b2c5086686c9700f717ee54da GIT binary patch literal 77256 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 P00000NkvXXu0mjfmHrKl literal 0 HcmV?d00001 diff --git a/docs/agents/icons/explore.png b/docs/agents/icons/explore.png new file mode 100644 index 0000000000000000000000000000000000000000..e1ce009feea23bace8306ad2e56b91c3ec934726 GIT binary patch literal 103701 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/agents/refine.md b/docs/agents/refine.md index 6646e801a..f0eae14de 100644 --- a/docs/agents/refine.md +++ b/docs/agents/refine.md @@ -28,6 +28,18 @@ The refine agent always produces a plan — it never halts to ask questions. Whe There is no separate command to invoke refine alone. It runs as the second stage of the refinement pipeline after explore completes. +## Control labels + +| Label | Meaning | +|-------|---------| +| `fullsend:refining` | Applied by the refine pre-script while refinement is in progress. Removed on completion. | +| `fullsend:refined` | Applied by the refine post-script when a plan is produced. Signals that critique can proceed. | + +## Configuration and extension + +See [Customizing with AGENTS.md](../guides/user/customizing-with-agents-md.md) and +[Customizing with Skills](../guides/user/customizing-with-skills.md). + ## Pipeline flow ``` diff --git a/hack/lint-agent-docs b/hack/lint-agent-docs index 1a7e3b7a5..b88b49836 100755 --- a/hack/lint-agent-docs +++ b/hack/lint-agent-docs @@ -62,7 +62,9 @@ echo "Checking agent doc structure..." echo "================================================" REQUIRED_SECTIONS="How the agent works|How it helps|Commands|Control labels|Configuration and extension|Source" +OPTIONAL_SECTIONS="Pipeline flow|Exploration dimensions|Revision loop|Output structure|Verdicts|Auto-create vs. human gate|Review rounds|Assessment dimensions" IFS='|' read -ra SECTION_LIST <<< "$REQUIRED_SECTIONS" +IFS='|' read -ra OPTIONAL_LIST <<< "$OPTIONAL_SECTIONS" for yaml_file in "$HARNESS_DIR"/*.yaml; do doc_value="$(grep -E '^doc:' "$yaml_file" | sed 's/^doc:[[:space:]]*//' || true)" @@ -102,6 +104,14 @@ for yaml_file in "$HARNESS_DIR"/*.yaml; do break fi done + if [[ "$found" == "false" ]]; then + for optional in "${OPTIONAL_LIST[@]}"; do + if [[ "$section" == "$optional" ]]; then + found=true + break + fi + done + fi if [[ "$found" == "false" ]]; then extra+=("$section") fi @@ -114,7 +124,7 @@ for yaml_file in "$HARNESS_DIR"/*.yaml; do errors=$((errors + 1)) done for s in "${extra[@]+"${extra[@]}"}"; do - echo " unexpected: \"## $s\" — adding new sections is fine, but please add them to the REQUIRED_SECTIONS list in hack/lint-agent-docs so all agent docs stay consistent" + echo " unexpected: \"## $s\" — adding new sections is fine, but please add them to the REQUIRED_SECTIONS or OPTIONAL_SECTIONS list in hack/lint-agent-docs so all agent docs stay consistent" errors=$((errors + 1)) done else diff --git a/internal/scaffold/fullsend-repo/.github/workflows/create-children.yml b/internal/scaffold/fullsend-repo/.github/workflows/create-children.yml index e0d98e65f..9bf4c0880 100644 --- a/internal/scaffold/fullsend-repo/.github/workflows/create-children.yml +++ b/internal/scaffold/fullsend-repo/.github/workflows/create-children.yml @@ -3,7 +3,7 @@ name: Create Children permissions: - actions: write + actions: read contents: read id-token: write issues: write diff --git a/internal/scaffold/fullsend-repo/policies/critique.yaml b/internal/scaffold/fullsend-repo/policies/critique.yaml index e5d040834..fbb12ce4a 100644 --- a/internal/scaffold/fullsend-repo/policies/critique.yaml +++ b/internal/scaffold/fullsend-repo/policies/critique.yaml @@ -51,4 +51,3 @@ network_policies: - path: "**/gh" - path: "**/git" - path: "**/node" - diff --git a/internal/scaffold/fullsend-repo/policies/refine.yaml b/internal/scaffold/fullsend-repo/policies/refine.yaml index e5d040834..fbb12ce4a 100644 --- a/internal/scaffold/fullsend-repo/policies/refine.yaml +++ b/internal/scaffold/fullsend-repo/policies/refine.yaml @@ -51,4 +51,3 @@ network_policies: - path: "**/gh" - path: "**/git" - path: "**/node" - diff --git a/internal/scaffold/fullsend-repo/scripts/create-children.sh b/internal/scaffold/fullsend-repo/scripts/create-children.sh index 579dfdc09..e49052c15 100755 --- a/internal/scaffold/fullsend-repo/scripts/create-children.sh +++ b/internal/scaffold/fullsend-repo/scripts/create-children.sh @@ -27,8 +27,10 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ -f "${SCRIPT_DIR}/pipeline-events.sh" ]]; then source "${SCRIPT_DIR}/pipeline-events.sh" + # shellcheck disable=SC2034 # HAS_PE used by callers that source this script HAS_PE=true else + # shellcheck disable=SC2034 HAS_PE=false pe_start() { :; } pe_end() { :; } diff --git a/internal/scaffold/fullsend-repo/scripts/markdown-to-adf.py b/internal/scaffold/fullsend-repo/scripts/markdown-to-adf.py index 595e0ec28..60e3dc4d7 100755 --- a/internal/scaffold/fullsend-repo/scripts/markdown-to-adf.py +++ b/internal/scaffold/fullsend-repo/scripts/markdown-to-adf.py @@ -8,6 +8,7 @@ Outputs a JSON object suitable for the Jira REST API comment body field. Handles: headings, bold, code, links, bullet lists, horizontal rules, paragraphs. """ + import json import re import sys @@ -26,36 +27,42 @@ def parse_inline(text: str) -> list: nodes = [] pos = 0 pattern = re.compile( - r'(?P\*\*(.+?)\*\*)' - r'|(?P`([^`]+)`)' - r'|(?P\[([^\]]+)\]\(([^)]+)\))' + r"(?P\*\*(.+?)\*\*)" + r"|(?P`([^`]+)`)" + r"|(?P\[([^\]]+)\]\(([^)]+)\))" ) for m in pattern.finditer(text): if m.start() > pos: - plain = text[pos:m.start()] + plain = text[pos : m.start()] if plain: nodes.append({"type": "text", "text": plain}) if m.group("bold"): - nodes.append({ - "type": "text", - "text": m.group(2), - "marks": [{"type": "strong"}], - }) + nodes.append( + { + "type": "text", + "text": m.group(2), + "marks": [{"type": "strong"}], + } + ) elif m.group("code"): - nodes.append({ - "type": "text", - "text": m.group(4), - "marks": [{"type": "code"}], - }) + nodes.append( + { + "type": "text", + "text": m.group(4), + "marks": [{"type": "code"}], + } + ) elif m.group("link"): href = m.group(7) scheme = urlparse(href).scheme.lower() if scheme in ALLOWED_SCHEMES: - nodes.append({ - "type": "text", - "text": m.group(6), - "marks": [{"type": "link", "attrs": {"href": href}}], - }) + nodes.append( + { + "type": "text", + "text": m.group(6), + "marks": [{"type": "link", "attrs": {"href": href}}], + } + ) else: nodes.append({"type": "text", "text": f"{m.group(6)} ({href})"}) pos = m.end() @@ -91,7 +98,7 @@ def parse_inline_multiline(text: str) -> list: def text_to_adf(text: str) -> dict: """Convert markdown-style text to an ADF document.""" doc = {"type": "doc", "version": 1, "content": []} - blocks = re.split(r'\n{2,}', text.strip()) + blocks = re.split(r"\n{2,}", text.strip()) for block in blocks: block = block.strip() @@ -102,58 +109,68 @@ def text_to_adf(text: str) -> dict: doc["content"].append({"type": "rule"}) continue - heading_match = re.match(r'^(#{1,6})\s+(.+)$', block) + heading_match = re.match(r"^(#{1,6})\s+(.+)$", block) if heading_match: level = len(heading_match.group(1)) - doc["content"].append({ - "type": "heading", - "attrs": {"level": level}, - "content": parse_inline(heading_match.group(2)), - }) + doc["content"].append( + { + "type": "heading", + "attrs": {"level": level}, + "content": parse_inline(heading_match.group(2)), + } + ) continue lines = block.split("\n") - if all(re.match(r'^\s*[-*]\s+', line) for line in lines if line.strip()): + if all(re.match(r"^\s*[-*]\s+", line) for line in lines if line.strip()): list_node = {"type": "bulletList", "content": []} for line in lines: - item_text = re.sub(r'^\s*[-*]\s+', '', line).strip() + item_text = re.sub(r"^\s*[-*]\s+", "", line).strip() if item_text: - list_node["content"].append({ - "type": "listItem", - "content": [{"type": "paragraph", "content": parse_inline(item_text)}], - }) + list_node["content"].append( + { + "type": "listItem", + "content": [{"type": "paragraph", "content": parse_inline(item_text)}], + } + ) if list_node["content"]: doc["content"].append(list_node) continue - if all(re.match(r'^\s*\d+[.)]\s+', line) for line in lines if line.strip()): + if all(re.match(r"^\s*\d+[.)]\s+", line) for line in lines if line.strip()): list_node = {"type": "orderedList", "content": []} for line in lines: - item_text = re.sub(r'^\s*\d+[.)]\s+', '', line).strip() + item_text = re.sub(r"^\s*\d+[.)]\s+", "", line).strip() if item_text: - list_node["content"].append({ - "type": "listItem", - "content": [{"type": "paragraph", "content": parse_inline(item_text)}], - }) + list_node["content"].append( + { + "type": "listItem", + "content": [{"type": "paragraph", "content": parse_inline(item_text)}], + } + ) if list_node["content"]: doc["content"].append(list_node) continue - table_match = re.match(r'^\|', block) + table_match = re.match(r"^\|", block) if table_match: - table_lines = [l for l in lines if l.strip() and not re.match(r'^\|[-\s|]+\|$', l)] + table_lines = [ + row for row in lines if row.strip() and not re.match(r"^\|[-\s|]+\|$", row) + ] if len(table_lines) >= 1: table_node = {"type": "table", "attrs": {"layout": "default"}, "content": []} for i, tl in enumerate(table_lines): - cells = [c.strip() for c in tl.strip('|').split('|')] + cells = [c.strip() for c in tl.strip("|").split("|")] cell_type = "tableHeader" if i == 0 else "tableCell" row = {"type": "tableRow", "content": []} for cell in cells: - row["content"].append({ - "type": cell_type, - "content": [{"type": "paragraph", "content": parse_inline(cell)}], - }) + row["content"].append( + { + "type": cell_type, + "content": [{"type": "paragraph", "content": parse_inline(cell)}], + } + ) table_node["content"].append(row) doc["content"].append(table_node) continue @@ -168,17 +185,22 @@ def _flush_list(): if list_items: ln = {"type": list_type, "content": []} for it in list_items: - ln["content"].append({"type": "listItem", "content": [{"type": "paragraph", "content": parse_inline(it)}]}) + ln["content"].append( + { + "type": "listItem", + "content": [{"type": "paragraph", "content": parse_inline(it)}], + } + ) doc["content"].append(ln) list_items = [] in_list = False list_type = "bulletList" for line in lines: - is_bullet = bool(re.match(r'^\s*[-*]\s+', line)) - is_ordered = bool(re.match(r'^\s*\d+[.)]\s+', line)) + is_bullet = bool(re.match(r"^\s*[-*]\s+", line)) + is_ordered = bool(re.match(r"^\s*\d+[.)]\s+", line)) is_list_item = is_bullet or is_ordered - is_heading = bool(re.match(r'^#{1,6}\s+', line)) + is_heading = bool(re.match(r"^#{1,6}\s+", line)) if is_list_item: new_type = "orderedList" if is_ordered else "bulletList" @@ -187,35 +209,52 @@ def _flush_list(): if not in_list and mixed_content: para_text = "\n".join(mixed_content).strip() if para_text: - doc["content"].append({"type": "paragraph", "content": parse_inline_multiline(para_text)}) + doc["content"].append( + { + "type": "paragraph", + "content": parse_inline_multiline(para_text), + } + ) mixed_content = [] in_list = True list_type = new_type if is_ordered: - item_text = re.sub(r'^\s*\d+[.)]\s+', '', line).strip() + item_text = re.sub(r"^\s*\d+[.)]\s+", "", line).strip() else: - item_text = re.sub(r'^\s*[-*]\s+', '', line).strip() + item_text = re.sub(r"^\s*[-*]\s+", "", line).strip() list_items.append(item_text) elif is_heading: _flush_list() if mixed_content: para_text = "\n".join(mixed_content).strip() if para_text: - doc["content"].append({"type": "paragraph", "content": parse_inline_multiline(para_text)}) + doc["content"].append( + { + "type": "paragraph", + "content": parse_inline_multiline(para_text), + } + ) mixed_content = [] - hm = re.match(r'^(#{1,6})\s+(.+)$', line) - doc["content"].append({ - "type": "heading", - "attrs": {"level": len(hm.group(1))}, - "content": parse_inline(hm.group(2)), - }) + hm = re.match(r"^(#{1,6})\s+(.+)$", line) + doc["content"].append( + { + "type": "heading", + "attrs": {"level": len(hm.group(1))}, + "content": parse_inline(hm.group(2)), + } + ) else: _flush_list() if line.strip() == "---": if mixed_content: para_text = "\n".join(mixed_content).strip() if para_text: - doc["content"].append({"type": "paragraph", "content": parse_inline_multiline(para_text)}) + doc["content"].append( + { + "type": "paragraph", + "content": parse_inline_multiline(para_text), + } + ) mixed_content = [] doc["content"].append({"type": "rule"}) else: @@ -225,10 +264,20 @@ def _flush_list(): if mixed_content: para_text = "\n".join(mixed_content).strip() if para_text: - doc["content"].append({"type": "paragraph", "content": parse_inline_multiline(para_text)}) + doc["content"].append( + { + "type": "paragraph", + "content": parse_inline_multiline(para_text), + } + ) if not doc["content"]: - doc["content"].append({"type": "paragraph", "content": [{"type": "text", "text": text or " "}]}) + doc["content"].append( + { + "type": "paragraph", + "content": [{"type": "text", "text": text or " "}], + } + ) return doc diff --git a/internal/scaffold/fullsend-repo/scripts/pre-critique.sh b/internal/scaffold/fullsend-repo/scripts/pre-critique.sh index 5555e42e6..faaf5dee3 100755 --- a/internal/scaffold/fullsend-repo/scripts/pre-critique.sh +++ b/internal/scaffold/fullsend-repo/scripts/pre-critique.sh @@ -131,12 +131,14 @@ if [[ ! -f "$WORKSPACE/exploration_context.json" ]]; then fi # --- Export paths --- -echo "ISSUE_CONTEXT=$WORKSPACE/issue-context.json" >> "${GITHUB_ENV:-/dev/null}" -echo "EXPLORE_CONTEXT=$WORKSPACE/exploration_context.json" >> "${GITHUB_ENV:-/dev/null}" -echo "REFINE_RESULT=$WORKSPACE/refine-result.json" >> "${GITHUB_ENV:-/dev/null}" -echo "CRITIQUE_HISTORY=$WORKSPACE/critique-history.json" >> "${GITHUB_ENV:-/dev/null}" -echo "REVIEW_ROUND=$REVIEW_ROUND" >> "${GITHUB_ENV:-/dev/null}" -echo "MAX_REVIEW_ROUNDS=$MAX_REVIEW_ROUNDS" >> "${GITHUB_ENV:-/dev/null}" +{ + echo "ISSUE_CONTEXT=$WORKSPACE/issue-context.json" + echo "EXPLORE_CONTEXT=$WORKSPACE/exploration_context.json" + echo "REFINE_RESULT=$WORKSPACE/refine-result.json" + echo "CRITIQUE_HISTORY=$WORKSPACE/critique-history.json" + echo "REVIEW_ROUND=$REVIEW_ROUND" + echo "MAX_REVIEW_ROUNDS=$MAX_REVIEW_ROUNDS" +} >> "${GITHUB_ENV:-/dev/null}" pe_end "pre-critique" "pre-critique" "$(jq -nc --arg source "$ISSUE_SOURCE" --arg key "$ISSUE_KEY" --argjson round "$REVIEW_ROUND" '{source:$source, key:$key, review_round:$round}')" diff --git a/internal/scaffold/fullsend-repo/scripts/pre-refine.sh b/internal/scaffold/fullsend-repo/scripts/pre-refine.sh index eef263d91..f7217f5e3 100755 --- a/internal/scaffold/fullsend-repo/scripts/pre-refine.sh +++ b/internal/scaffold/fullsend-repo/scripts/pre-refine.sh @@ -175,9 +175,11 @@ elif [[ -f "$WORKSPACE/critique-feedback.json" ]]; then echo "Critique feedback already present from artifact download." fi -echo "ISSUE_CONTEXT=$WORKSPACE/issue-context.json" >> "${GITHUB_ENV:-/dev/null}" -echo "EXPLORE_CONTEXT=$WORKSPACE/exploration_context.json" >> "${GITHUB_ENV:-/dev/null}" -echo "REVIEW_ROUND=$REVIEW_ROUND" >> "${GITHUB_ENV:-/dev/null}" +{ + echo "ISSUE_CONTEXT=$WORKSPACE/issue-context.json" + echo "EXPLORE_CONTEXT=$WORKSPACE/exploration_context.json" + echo "REVIEW_ROUND=$REVIEW_ROUND" +} >> "${GITHUB_ENV:-/dev/null}" if [[ -f "$WORKSPACE/critique-feedback.json" ]]; then echo "CRITIQUE_FEEDBACK=$WORKSPACE/critique-feedback.json" >> "${GITHUB_ENV:-/dev/null}" From f3164da1d02f883b01df0d8c6f26dea05d841051 Mon Sep 17 00:00:00 2001 From: Adam Scerra Date: Thu, 18 Jun 2026 13:28:43 -0400 Subject: [PATCH 4/8] fix(scaffold): add missing env files for explore, refine, critique The *.env gitignore rule prevented these from being committed. Force-add to match existing agent env files (triage.env, review.env, etc.). Signed-off-by: Adam Scerra Co-authored-by: Cursor --- internal/scaffold/fullsend-repo/env/critique.env | 7 +++++++ internal/scaffold/fullsend-repo/env/explore.env | 2 ++ internal/scaffold/fullsend-repo/env/refine.env | 5 +++++ 3 files changed, 14 insertions(+) create mode 100644 internal/scaffold/fullsend-repo/env/critique.env create mode 100644 internal/scaffold/fullsend-repo/env/explore.env create mode 100644 internal/scaffold/fullsend-repo/env/refine.env diff --git a/internal/scaffold/fullsend-repo/env/critique.env b/internal/scaffold/fullsend-repo/env/critique.env new file mode 100644 index 000000000..c743bd3cd --- /dev/null +++ b/internal/scaffold/fullsend-repo/env/critique.env @@ -0,0 +1,7 @@ +export ISSUE_CONTEXT="${ISSUE_CONTEXT:-/tmp/workspace/issue-context.json}" +export EXPLORE_CONTEXT="${EXPLORE_CONTEXT:-/tmp/workspace/exploration_context.json}" +export REFINE_RESULT="${REFINE_RESULT:-/tmp/workspace/refine-result.json}" +export CRITIQUE_HISTORY="${CRITIQUE_HISTORY:-/tmp/workspace/critique-history.json}" +export REVIEW_ROUND="${REVIEW_ROUND:-1}" +export MAX_REVIEW_ROUNDS="${MAX_REVIEW_ROUNDS:-3}" +export FULLSEND_OUTPUT_DIR=/sandbox/workspace/output diff --git a/internal/scaffold/fullsend-repo/env/explore.env b/internal/scaffold/fullsend-repo/env/explore.env new file mode 100644 index 000000000..021f4f4a7 --- /dev/null +++ b/internal/scaffold/fullsend-repo/env/explore.env @@ -0,0 +1,2 @@ +export ISSUE_CONTEXT="${ISSUE_CONTEXT:-/tmp/workspace/issue-context.json}" +export FULLSEND_OUTPUT_DIR=/sandbox/workspace/output diff --git a/internal/scaffold/fullsend-repo/env/refine.env b/internal/scaffold/fullsend-repo/env/refine.env new file mode 100644 index 000000000..e1ca9d933 --- /dev/null +++ b/internal/scaffold/fullsend-repo/env/refine.env @@ -0,0 +1,5 @@ +export ISSUE_CONTEXT="${ISSUE_CONTEXT:-/tmp/workspace/issue-context.json}" +export EXPLORE_CONTEXT="${EXPLORE_CONTEXT:-/tmp/workspace/exploration_context.json}" +export CRITIQUE_FEEDBACK="${CRITIQUE_FEEDBACK:-/tmp/workspace/critique-feedback.json}" +export REVIEW_ROUND="${REVIEW_ROUND:-1}" +export FULLSEND_OUTPUT_DIR=/sandbox/workspace/output From 6728544faab1cf7d512b5cd66ab73413efac02cb Mon Sep 17 00:00:00 2001 From: Adam Scerra Date: Thu, 18 Jun 2026 13:40:09 -0400 Subject: [PATCH 5/8] fix(docs): use tech-research icon for critique placeholder Signed-off-by: Adam Scerra Co-authored-by: Cursor --- docs/agents/icons/critique.png | Bin 77256 -> 71770 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/agents/icons/critique.png b/docs/agents/icons/critique.png index 5ea8aff25158f74b2c5086686c9700f717ee54da..a385bb652026db7d0864ddc5a143da1470f7adb9 100644 GIT binary patch literal 71770 zcmV)9x4-Y(>VgmdmT+$UTrCknDRtbRDFsRdQ5pn93CMH*&bjqp|FSRl0i0W>ceO9N z2ugvWCR7#5SKuo|8p40lsYXzLPUV{9f}mB&IPBJYn=-aKW8fj2gp=F zChM4TmEChWJC?e1iwx0*stT;EudqJyRAzwbEK`%gDGUQr7_>u`$JbJRm&TPc%UVS02zUE?fEzZ%+HO3f5rDtVrgU1eS@Pj)Zntk*# zPbi1fu0o^8^A5`9R1_RIxXq`(dJEsX?D?(ip(syXB(0PWo z8EB21!$VH%I+sEG9IWZiEZE5=dpZ+-=RuUx?p2gvf|oLnrSdC^QO{K~iu*JE~c;LWeS8tsMzKgJrcw!=O5KExxBK0&|N=h|zo;=Fx3 zD9df|CE9Ou)y2EH^1@|~+_wd$A7Aem@nX*64Ad3X+8S(b&}*=fszOP?)fGx>GLxgS zoZ5Tp$}z6SRFjh4&YkF@I9GC<;&m=S{MWwP7e~-_&3Sw0*|lSUpR6L{$+9kwJoF@A z{^EBzy0%ST09X9jI6K(VB#i>hhs1~dC>!e(dH&2pto}eDlblHcjf1OoeK~@Co6V0 z9Xl46$u;0TN`o>Pk3V&U?U7?{eitZs{K;e7ci+RDxA*mkDk0QpJcB`o) z2hjnl%}v6_I&%^*lOc1;@ye?%=9&x7XSqM1lNWekWpj%Mo_vbC9(tU^n`?A>eK3Zt z^;Ifk=s)~Nd07G?o^G!L;t9c%8;ddquqbT^C~|EPlQA4l@Ihfr zhUfrzj8bSVvE%SUzdpqjg~Ct9xXmpJS7Pch`v(Ode(T$L&E*%fkQp*Oh!9W&_VT)` zFXR5JkMeil{vP)oI!5U9C`72Xwy^y^W^NGkk{27d&aG!%=K{pf&5{TTPAd+?d$fXn zzrbigfF5tY!ibBN2_TvkD2^*3VqEHeD;}NEskOxOmm%Q^;uj3up zUxOL0Asa)4fH4N~9;GtoOwL=bzL>%){^--6=fG%-yfe>aGNB%hu>C&L9@Du9aWa>F zxewsnI!#My(1xFH6j`xgt`v+ zz>9hbBR-%AzQ&J7SXZLU3775J&6}>h8a7uklWn@bq8mKjP%+R#Uuv?+7Vg+nyz;{R zyzc7D$$gEgD@+K~<1xO9ZPA8ob#9%!buK{syets}&HBcO(hE)$qBNnNuxG~t?|R#f zTzLKxmtMM?cfI>f?BBl&LXGpDEX$}JY;27hisOq~N(t1SP);!7$%Eta{d-vK7)&)G z^A%P+Mm#zMG6WkOh4ScXf}U*hx>sDua<_x>6()FGIVSk}TxoHN*0~bnzxI_1b5V+5 z3m!eR%2P+SxbEU*%6d#y)gW-)bysl7r57Ryb8~&_dV&iMZ3}|R5!2zRqg#m1c+s1* z2;O^?2#BC1uy^MUGVf8|VU!{>8t*+46qR!TWX2%MGae7QaOX06cP#Sw&rTPyrv>kn#+g3jY&T!O^#D&DJi?oA zxRlHHFSBpU*(p24)C4Z9_EQ{OWoN`E&4?hlN5qcidqDv4IbhC z1CR37t1hL;J2*GN1cxyi2?3NsYmE;Olp*iUb8tB3(6JS;87=_EVC=ca_*AWPSN1fl z=X#k=7Xz>f5*|%u=TO>k_mhWs^5D(v83caw@4cI=F5QE##}wTz_uPGePkrG#Jh`RW zUVR6BTTtz=vqwC#Y+@y4)OISw2fvu&5Dy1SUD4&fzwK?DSyWzgwTDWK@FI;r`@ zm%q+kj~&D=?jYn{CIKoZQ~}08;W~ZH@@{Uv_dfpio8P3=1;OSxt?A6qW3r48!t@-^ zt&_IS1&C9;#CyD-kY_c!77Z8d>#%Qk&cXmJCBcsfQk~|bQUnYml{3!fQQc)sXAXzO zi9*>N-5C&y1;z}RUZ+t7qrb3&CpXvmlTUw+V>iBr_rB?M;34RO z`wt%C+jri}%?~`xx)>NNu@w}q*JW;df_V~l=9opKm)Kacxj812G3988@&VhOXKgs-u7@AzO*g!T?)*I4;|ZVt?hp9> z1CO9~?!}p$?Vt(09{oM%Vdn<;Mml~@h;d5Rxd3qzmx!QHgy0x-;B_}#%EcG-5w{BE z1}Zo@xyD#QqY0BSTIamsx_xvleC_-9v+Xp>bn&M}!W=)}2T)=NArOKmLm{G(sERmg z-on5=h7Z(Xi zr_Vem^*&YWT!1)j_`G`9UTqlwNv`=N4m96TqC7bQhi2Cd_VTyQuI>s)|1$tyU=@?6fv7cSGWjxZXr z*fCsk-cFVW1sadSp}gP=gBExhp!fJ9h2tv3ohkm+N$laM8YYWSs4HAOVQ$c)liODL=~AWPT zBiYS0qDY-I5TX>}6a*+qU!+684~I}q$h9UD&EjB=yeJT@C~Jpz9+buh4_cqoWu4}A zET+A>pUg3Pu{=;BP*%9@b!=H;3FHR$@7_gjHO3m0(YO#m zX@XYx5YDN(PxCsf1B9fP=h^-`-K*gg96h$p_Sj)Nea5xp@Y*J|GKk3tCZ|Rdv_Y%^ zW0*Kc@PS-sWJ>54nnH%;QleEj=~ik{lM(-3&;$?O$7>dX;6ut>B(gqF7Esx8nM6=^ zO}Vy$8g7$o@WV}Z&(Cw^WtSk{~GyM`owaGL(KXmbvHFi(O}RfM~P<&-T~L zb4->aE89GEc!*O4lVEsiWrHV=t+VYF&UVn<0l{_|)rxTqwTt4UTx$vgS6sM@eM>pI z+CYc8-N{c)(2JahAc9v@E{8}22~QSCIptOawD(jSt5hpT>8L;#!ELT_<8@ba#YOw^ zuEGPG;|XKuKwHonV=QtGgE+10?B_uQX&-2aAe#~uTXd&6Mo98G7*7KC-gk)Qg-h5m zw?NG%PaGYxH7uF$=NP3>;;FW`QK4e*j%DWiU8-_|ltZq$@&ewsUGZmscN@-aK)wSf z<~Y&QOFqu@8A?J75?Z5`A+!c9DT6o(L1}h+w-JTg+QO}^(G4}GDk-x%x)ojkdEj9LWI3YF*BZls}~gCS1eIy(b|2n4~qnyQ>ol~u#xjR0GgV~dO| zFQTsCG(S;FTMix_amyV~@P->MV0mFDL2a>8)~t-i~G!Hs#U7 z6y-KOt(a`Ca@Fpg{KH@S74|O9v$eWPcVV7K4jtp`-~SP0o#^{u3rrE`adL+7AeznT z>BsYIe||QtvnxPI{2y0U)Wad=aEQ1%+LS4cLgR_<%`r$6bOt@LEKf|qQ~O}ezPv)l zqmOS;`@pNOJCA)km+5yEgb6-4#0#B{rK-j}wOW$d9y=FyQ*EzNmJ^Du%Vh)|D44A<5O^#puPrY+hSL_C5ggb>?_rKc{(h_8|U9JbpzE&hemmscZ_ z^Tdc)lTgHI-l9+;mKCC=*EN`Xibohi0%Ogorpuq&b;bvX_9gJbWNVAj#u_pilN-SpaBiFJ;TR_ltrgvF4`X`} zEDD2+M{L)QzARAm`;nn1r*;Qw>wBd@dq6#XJ zkrcX3Azm2ex;-WlG=ksWW^&{Z-N}$Hfyt4BT)JzSfAJ4~hifmnkd-5c>CVlwUVHxI zCqBiOzkdrdzs%ScIGxkmwTo;phiH}Nd%XM^M4KswfYzR+#Q{5(<{9)01jW|&gpG|& z);5NC4_e#fugzJq&iDWkFQ!mWMvPWhagz}pBj6pQ;X2)c=E|%0F*o01QdS%~xWe|v zDn|D(woeFxD+Beoz~&hy%TDVw==d-jlq1>MG3zX`iRhPUwN8a7dj~{%B;h%qj zkA39rTzlz0s^KwQIm8Cx-EVvaS#J5{S8wOZk;i5`a5{fU=O8Vd6QYVUfD#2E%y=yD zqJx79LcO_3y>f_RxJ^eKZgYjJ_wV7K{?^BM^%a+~zH*G-{34GXTjk&Vg6bLCYRV{(NA zIQZl$pa0x9`SEQ(B}dTNfLv>;@erNo^lWxox%}+|krB?IN^kHgqc?v(_dWGE zfA*CEX;El#XaXv6 zN4)p6)Tp39s}s&3rpF+t5OABDR7VceDYxn47#};tmFMl|U;d-t=M@)T!1~HDioqQB zJ@FL(`(OPvcRuDov(gu$Zwte*uGjpWA+NKWiH!jLBMp``NJ0*Z|Qi1&oJ7>d}x)35uGntrf1k zYCr$vpZ^y7_s#x6*a`PCz8llE3yy^Oju*&dfpZ`7w zwvJ*uJMrqp0hgkZpnHa)s8g^ZG*i4!j3{+nSs_Bbv4LATO1BzPxRUY8QEt5E3jWD& ze~e3ZEi+tSrN6Mq&3D|*|Nd8>;IYkZ)Q;VZRL&%e!J$~%K`|h*auesp7MI>Ct4{yJp(a^R2nJ;&+JCSup28h{<=Uq*? zwT-VTvP@IElA^2l=&$|~mt3-wjjbo}euOrHIE|}EEH3Tl*MIAS9C++W?z`s@Mczjw z5b6n`cIf0Ma5~p>Jq#j(RvIUPQ78z-9v)dg!XN$3O_U|v_^Qh=Y7IZxLi{RkdiBL9 zqxh3ge}|{WE9ma77Y|&j2>z--tF8o%=-A&lQ8$P-I0R3k^nD#On$Fu!v*y4Od}G=Q%GCGjjR zcDe9^oxp@ld5TU!mVxLvIn3lH@(@K(e!@lP@8-y{P42z_aX14qp%YzaC@pO|+N~as z@#QEgA4AP#w8{0ayo#%@xrpI-1usLCcI4KR8IMvEhQp)G&nZ6q;dhf~f>aZ9aIsto zzTFYe6c^<7VNn_{h7n!r{yq+l=lKtx_$FWf{{86g4s_llc#j*d@`h_KKAujGR~|lV&<^9MS0{YI@LBsHD-L|Nq*_Z*YQ96vwzG| zH)ArH&{^8SXTJ4q{j z1KoZHV}uYK3Xjr}g{qBUbA<~vMmzTH+d-$Wh@5;Ao>R5X%mC5$y1`WyzOFF}5C=Fe zym%kofu*Xq0gu)JCpBKxXzhWD?a?Y%TyYUQcP~;_Ba8&J#I)~a&sCpgHF!mIo6e~_ zyI9k^_%EOP4xj(}?Sx_(lXs}gG1c}l-f;be{N9J(%6a_~J6os34nV{^sM9g9@e zq&?L$Xj6K|HN`6wwHRE!$d=y8Uw!F^eEu7^kxcJJzC|puh`FTec(;(>5ft3 zV;Fg1S%gBh!*9j=r;&dmNcn7_T2s#G5U95{7#}=Dx17YX;>cm%_4*t5@BY#6lLv>Z z9ob-!Kl{vQ`46A?6lLB=dJBwnhFV-^ux~%6-;Z33mq~2*+$)tALFjf1I-S@-GFr{x zxRHa?sM3rvv2$SzwR0#PZS~Hkb%qBBL80TcXA!J6h!e)c30f3d86JHa6Y9-X-u~KG z@elvrM={le5In);{Lx>3lE3=$SMdEse0QF0EG#V2-MJS!aVE&@H8_)3j0Yi=Xj7vd z-IC;{@NVrxnl~yZMcZ;Z*BSk`D6Jv|SjC&6ltILD-+hlT+$zblKJ`S98Zwiib%v`I z;w_o&bL{XYPd#x2n-vjwJGs^dr*zFSGOM7d>^x<@kI&z9CtvvHZ3HtQ%L+0r$asxk zc-@t}@%oFg^)}{2t?H898wymIDRCrmHF)N1e@YS(aE{5!3VJl8s|B~U&c(ZS@O!`d zQFhJu7>|b7ZjVoX`D^^e-+h6yGr)KIYz2*4TB5&eAG+6V(3AGu&%8A~wyLTamvt1k zc|k;_O)L`4q-hifA&B5=hXjRpg7fa|c?i$g0MQ0nh0ZKW8N3g8FZBEK+{B|wjEXBm8#Pkr(WY^;y5roed*%Ak!o!ym-yn))^>Dlt^WBCdY{ zpZdxzeED0qVY@rf)=-VN=w+T?c*85$)d$?@gdf4#YoMN1ny6>^yV<3cAmxO5Ji>ZM zD94yk@t(K5iA(qGq8x6J^*Vg#w%hpl7ruZD7Lm??k;%z-?WDhFA9OniQ{BRue@`@% ziR19mHC!+RGA5InkUCL_M!clJYpF_)4`6hMV=JqS$K@G+jG*L^#@QY||Bc(Z=Yc0N*&NnpxO&9-dzaa_qd?cAHj8+^T3#ADrPc(Z)tu5A zQSqPCPd)vp+Fy2)5-Cfp5<)p-|DI)Deck2wdWL7R_{iSgM#uJCAg3axk$DTMwRi8YM zjnlEt$N(|BbDFH6=yj2%{U(}TXP%=6xB0jK>!0wYzq^^u6^+z$XXAy=R*Yx33)XETgOB@{=zIW?SQCS~lawK@>dO17h`E~fKZj}U0!DA;sU;$(CziO>wyQj_tD4Dy#W)g zv5QM|mv=*w;-5{c;gf1**xD|+>+T0RdTbkGyM&N28rC=$D4l0gI<~gQXj2eE#y$5v z%CTb`Sd+mS)M9_Wbw(1Uj$0Z!gE>YUo7AHkt2E9D{q7ROjV=E4k3PdUzj8AdT(}>l z6^}gdIES8GCGX5*br&y+5QN-jm@IFD_u0Z^YgZB}1r!w-rdZ~|$B(kMHDSK!P*xGR z?R1Km-nOKt6aIqMat zMDQU3brOV5o?)$pvPPp>-`WO~MXNZikt~a<%(F2_Kh5(4ZCTqG^MhOOWqoxIuYARY zWCkR7Y$szfbo}J*M|k+LWBBmW)VEDW{j@(k<=5|w4G?WziUzCbEiK`yl6rH43{cwO zyvM4XZZB_JblwM?3yD+}t><)Ru-y(i%i4$gbUm0aG?jeL<4O;Spz%6m>@DNcp=_Mv zX_QW7fybN(O}gcoM6QGO65Xbg5;Mv>pZg;kTl~f*Uz_;-DvlXrQdUT^vQtX80=#GY z+!vF2pY9-Ye2~bPR5cGhdXNjwTjGNAmY7UNXi==JuJh=lhj5a=XxyRso@YGA(_lz* z#s-LHH79R7T?Wg`tdA2YX28{+%osvl5quRr4{e6l7ViUDmJ`x!2a{zHd`r~8)28M; ziQhBJj|e)$iDojX2qBgPL43@TPPm29JY$k}_^2glUUqnX(w|Z@#H=9X#$uF4lrV8M zxMa0A%PylQM z#FUzO$xiKRJp;TT!OdhuGZri}Sz`ntb?c$|oHtxWyLuO(s$J{RsRaxsy@n@voaz6K zQJ{&#?%L~f0-quZqcy>K%klV;xmaVjAm8cI_JQ&;Rfbc=F&={OOKY?9AqHxQ(h!-T2 zm-yobb;2<)LZ4Bd9E*(*LSzY^kd{-3VL+zWw;4T$}@r23N zCgKD7=~KOQWOzm!yx5LKYl?o4yy%?LUG%wDBmcHqv3q%*<)t3u0_Ns+a^E9sGhp$F z9al?rCy>hoGv1(36L$7AyO(>+b$Tcb>lT3wsU(RS_mB0SXm#JI@St3R$P{~)|^sGh}456;5x3|$D zQpJ_{>!$tG_ z_AkGki}%jq#v6os169XzHOFwx`Ey)+-dnhI-~IgUO?Ps{2_el;;UwWXXH|fR;(_VI z6(6<`r)Zmi4>j{$OD8u-s89(Unx*od)|KX88WdJ}j4N@~5Upx5Z77|`76XFG$!tN5 zk2Jg!@orA~6zAhC3m-xpz)MPLPk_TfnlMf?BuA+@u-E&j$4HsUvjU1V_U=rF?5KcJ zLLjtVlK4>?t!zRSo{hR44_QmVDvu5oRk?*yLZ%cRgMtV7kli*@)-<jZ zbvT}`{wzYbXZ(ybdCZE#j@P(ycZc8k$UC@f?|^dSFrgaJu{zS*MZlNT8$;&icksS9 zzY^z!zrN{iTo@2^7f~n2wdyPj5V7s4HF3uzj)pI+A=4niLGb8ygq$b%WTU213-7cB zinzlW1aTF9vc(P8Uc!5S@hucZ##4um@u|;#oz-ELWWqWM)SiXFc-%^A!O)D)Og8n6 zU*d_N2dYgrgpeLQB~eGvh>zNzd*(xEf2V1WwmYXb_@rtCg_1N8OP=HU$MGi7TVgVz zV;q0~!|&$mt1iW6mWLia#3w)dWwxs^K`E5V6JYx?vl1J*yhxhS()fCuK%k#3p+(U3 zn7$hFOTX|IF5kbzc=d5ID*By*@p#O}a74FTFgNJqoTD0TK^}Pbjj!N=$B*&7hd1$^ z0w?;o&wjD%EDI1#ODz(6B*X{Ln5e32Esu4gvPl53iSPe1%v4AZi$XBs@%4xsUvoA8 z@HaoeWHMrNT=SzJ{)i*@JdUwBDvQ~GoXCNu7Ntt6SEEfj^`xgbfrp}H^ohjT7)e6k z_++-4hsF0FT{`6 zu{uzgjAMsaxb@aMIec`Le!s&TUiWG)xnw_nG(_Bx`Tl^nz4l7(eB^u7p~PvOWalsK zx}0SJg2<$7nS${F_Rh!7fHD|sawDy;43cPx0j`Ufo!dk8$(&u+;ZF9+wGo-2s z^%fWJUuNHOhqcub-R;H9@+D@>XCWb82n{fowo#)j1ceYV+MtuCqE3BzYb=|5EWegO$p{F-SJMKk(jIc1@jRH8O$ZbYdIS#L^;mF8}0V*pvbZnKQE34#LhS3To zfnJfZYk3~=^$TcoKksRsMJt`OGOFG6RO)J|WQ``y$G|Wn2J$b14`pk-5<;YVO9cO{ zg2HIT*W@M*g9!<`6LsPNztv+kwPxZkXfRTtefb=G#b-L+kZ_x6Jw5YP&%Sg_@16IE z59z#Plu%m7fZg1W_Kkk#<5CJ7p%rqe;K^*9gq|DXOA{o5nQ8t8A~T@p%ge%)l+DCa zFVWs#KoSwlMJTc?WnBR@(J|XveDG~c9E3c}(OQ#50FF#6`rZ7b`mUZ;0YX&tEihS* zH3p4Ovk;?gnoie6g{FfbQ}WPh+FTkmI)Y9CypQcoKnInc=@57SP`8H_v!WNUW-{x@ z&}-W`gH zK}qavC>;T_=7u!Gd{0`=b)v@%1m~&i8tr`&j>X(v(hidwGYuwMM-G9UAmr9i7ia`r ziIjv!l*OwPz+BpLE4Dck2@=ipIG?M;Xpd8pcZ4z1xTO>rg*K*T8ycma_dclfyGkX= zxzKp(qqN|B-3I6`-OuUME_4BB@O$N~A87dX_eg z2$@z$-INZ~?vvJFb#V@MQM6%;Dyw(pE+R6^Jq9YCAWp4K4I@P;G}!j;%{N&100gqX%hD6h=|LI^xJ zH7PEI#H_?Sf(H?Uh{l+lAPOH;>Lxt@QfVuWicp0FjbM;EM7C#m23ph4{6T0z&eD6F zfG!HWC>)V$ZIp_n$MB3FkWM**uCbET)DrL_pbgGV%MJ+v@iV&6(|m8AzFM+aDzB8% z*vwEA8jX7a!a0q%Jq~YszJL2qkz#<@4rT4BCKGUD_UxRa-!;_bHskSx+6g+JqoT`q zZ@HVbalq>t`s>eKn{(D=Ri%eFfD#k`Kg2O|&1!@5>6Kx`3ZUg{#BP8O(W?+sKpwzl zgr;Qx7#)Y+YKg?9r>CQ{iqP1@v1nbOvpJLT5i-0?8>u?P5eDu!c5BLr zTu9%SC@ewY#S&D3H45+7@jkX$w6^$JQJLtM-Rf~xh^>{uT9Ycl=Lz^Tie)h!%50IZ z+4K25pE!jemH?r4_|XVzPmq8Mnw4S6lbhpI zS~)fqN7_CwH7PTZa-wRTE z+-QvJO%R)5w4P2Q*HdlDv)!+g_b|+Wy$SOQAut+y0@(>A>dmp$%Am7(4v$Oz?6coy zGAViED=(s-H*HjxIBW*V`)s+4uYc>ueCq3WvL;K^#R8(TDdqd;E)qJmGt`qYwTCB< zZlX*PYkm>R?JZngqp}Rwlv1tHT(osl-l57N+K>6fr@q8Z-}*kjF2UqH@ZbT~R<8Zoh4gYtEnN&9AwNeh0vz zZI4^;e2A}n|8DMia>9n)P2JmpQ(0Sb{~U_78j7((8P)#1N%6oZw9YisjuI#sP7;RI z+OxH!AHlnT)Q-BUsKyg~S;gb4N=kZoe-QqY%6(w9V03mwHr!t_hMl3vEj9wF~(@TQ;5zedIP-kglZF|b6hI}PgszT?8u-LWeC`!$F{4u|M5d; zvgo8_;!7G_KxG|N@Gu^M$#97yV$g{h8Lw$odvxAqtDNv}|L8A3A~sV6(0LzpM&%sZ z9cKjmj-H%gDQ3ie8T_&k>_fSvJw2G&;3$m4MYzGt>aVFXZ_XrI?Tr z@@2NX;ntr%!ov^T%~e-iOusje3mUC+?tSPGKe_KPe(4hG&JLVpZN_l2&hP9C5UHIR zeFk*fCTzx<#qLI$Z=_}bwwD+rW*;ggf^!}>DH(5%36n9db_D0hGYc-XeS~-%7NrH0 z!F1*@I+jEkil9uQCdb`fDbNyuSChUkgxKX!DmgZ36=lU3y8WdnA@@Ea69O&>)}842!{AoeW$^Sc3o_qirlVf{qidB@VDP zc_&K1m5Ta{)OFHICxBTY!ISGLFGLX{=pIEKF>5zP)TpGuoTKq=_b1Lb(a96TJ0Eo@ zO7LYJ{Y!^Ks-j0e=u-3tXrvYDvnbir5# z=+17k%wY09+GO#XX9anG0XmCJ>>M=~PJvVS*%u(1s@`aHo!FC2Up%N}4=g&NM5k7t4w5<}HA-tdv1O`J)ODR$Y>I%OL+UgM zprXJjvS9;3LgKU}ySgAKiGMB_l=Xxk9rpY ztqob;jW23YWHv(rRO=hKvSeNY#bAKa=A;D)X;ihTovu~7p3OJ_HN~k;BuF%y5lZ2e z#i^VS3@*g$;FD{3B>|thN1|pa=O=HSeF0)>dl+}G5Y0`RK`&NC?!Pf6sTyDWsI%j` z5_IjaK@kbtE83NG!eMTel%KICKk`&g{x9Gcl&+jrvk49rtjFctq=G zN#hJ9sf1IpUDo_gnw#O%%WAZ4`PkZMB-Dw~8oM2Wo@uo<;;)p|=m@-q5HRR;uM&-# z5Uui4dss>QxhBxZ3@nPoYUgov&3L%Yk;6xM@WF?9=z)h>-`v9H1qMMIgB9>)$!L8Y zr8N0qkY4JS9wg#sWzcA}Hf^vTO#;I6+Xp_&!;ymTn7F)-Tp(Z6ZL_wIVMi4yLUvo9@ zfA70^?Q37n-aUILGK-K1KPbFP1Y}Q|8zD$gIPsC#rJ5rT5&UcA)H>}xVY;h4?bsyA z$x$I@0urUxjon=%j1Qsd=pE(%vqvI;i+0@U4TfHcgsFR6jqyf6Ow{5KFr}bPQnXc3GogAHySZq zS;cP;>Drhy4>#7>xnqIf`|aQ2eeZh@OAGTYpDRD?i zj)c_KYz$*{N>5Yz-D)qAmLs%=4Dsbsi3{QrNNbgjK9E`=Vk1*CrK1v>2X?B+YWy2? z0sz(xG7o*Hx8LIJ!tqsk* zwU|!Pe%?zhHA1>~@x@gXnL$Yfx|m{RFZesAtk`Is81H+^6BC{K)ai`Qx&YBc{3iXP z*>{whF-1*FJYr_vp%c6}vsUaOe*RDY{118Sn{UKb6GmIB z(5enpVp4`UHCrTk38v##Az8XL@{;r->c$p7eTr5sJ5N#oXaQN`gBYX{r>V4qE3B4w zhu20&relb~NM=c9K~iZGrUP@N!5AQDo$!Rl22T;a{Te(ZbzZdKy~iliQm!MLRfrUn zlr1#s+GYTNVyeI;Via|V;3m?@zLIP@o%PgkrcJY2$$|F)?t;VB`j&$(MdFCwb1Vv40W^{c(GiZ6g?%? zlOYKb)hR41X4hG##ZYQ0Fbb_HYV+_lNOE!)=EaFPo8S;!O*I@+Zf#>k=vc#OxXJS3 z9RJt<{$KFUx4ntc)*5v=LW#!^ZO5$Eu@$8h_!Mxo#@dDstP=Jxr3c3y(j+CLZg=;{ zT8wQ|r6e*6RrJ4zKPy>5=*(hFOy7+$Nly^* z2BYJQjW$puKZf|SMw40*k1?tZB&xB%SFs)2$lF^ULTi>Aqhtul#Y#oPpN36_VYZBFhY!u^5EW_6Cb{UHH{-ksZNLR4Tif`me#v?T zl~_`uQs$EiVPjO3-A^B^7yQ}nrjll$1}`&XAmX{7N{abemqE0tV5(`pq@-9GR;5yz2D(=uYDD3>&IfpfH?EQhuDkvu1*{c zi%o50 zR;Qwr;JriH)E=8j)1x_c(``xi{Ha{gNEA>t`@2Y6Ho4&1QmB!`NP2lgYPDi?lrX1r zYj(nrTC}b0Ua}R8-V~ul=q2etw2u3=))BlDABk34njoYoij2u{o4n}o&;QvUaP;V5 z9(n9ZvW_A*23MApqY*ZHF?^Aj^q*$6l$96)ObI$Faw zLBy!0+^{Hz!wS2~p)sT?yhO><{KNF+oCr6fnmm~rEj|e_yPgenK!Y>5KY5W<+ z!y&$|<4lICrZ5>F{?Lc$<~ifx5Uq$cehBe_HX5Z3gh1_FBna6ow!W;1>GO=jQgJ-q z(Q@ch@Js$OEx^@6lse+&UR)ZA8-K>C2ECJ+biAS7>h=;e)L+}YR1!0he1<7X`W{I* zX5>G_4vB68){IUuh)*R``tAlaZma-(d{IpD(M(5j+%XE$j_HerG0mKb#n1&#_jBlCR34Sj8bw1@=D0F0-NP*Z%=sq;R8JO z=o74OZZR$=DR`y+zDR5C(NdyBqwQ?jAZ@MPsPw#qxVxydv&M^&g3e646otl2gHhV0 zwbs-5mnm%zAxz5+)!NWbfz#O%Yz8X~G|JV)WL0Kr;ZK7ZMb}1^I1j#=snIs_5}u@{ z68Pqvlu}q@>2OxpO5qjMlxJOl zP^xMC5NrM*xG>uaGgy-{iV#(cA(a9#RXxEg8XR~R`|u&)s)|>>>UtLE=W+G81-2R; zNDSnfa#EwRf^NUZv19Apbkmpl_IJL+Bac1C*48j#4hp61G^{y&MTN2`| zMq?F)u@SIRnu&OntKtktHATap^SFJ$Ri3JL7;O+=VXbh<#TR0%qMX#|gnI-D(PAtF zTnK1eV6#5A|M+hH(;xo{xBuiWP>^>!=%R=_IY~Q^umP1sO9rC)vDrP_0AbqcV5AwI zI=v{8c4Vb=8hjIN_?k9h%f=I%h)ur54@^5;R!v)|YL@ogAoL)n?YPD4sU5AS67M9I zMq##Fk%C&olC(<4X=kau8ZGWM8XwuBsnyw(KuvwE6Jt}F>`G~T@I3I)? zgcSUm((NU`#Yz#<)b(aFk(BWntzKTDlssD?n4u@01Ye35wiL+>HS+y^$|fWP^2{*k z78o4~J4OQ4a6~p=p}V$Csh=xI2!T*f@Z|&*V%?nQImA}- zbKC8A^8ft*{?9!2#3AMvmr**F174!)(yPY&MYl+wCYrtqOEAnr(F%zCbhQ^wnt-> z$*8IcwywDJg8lSz&CrdpS&wou!VNdcvJ7k%CqO+{N-5wg*K!)_($UKcy4`MisAKC1 zJtZNbEQ7f@e){0U{ICDNf6bx8$5>d}iAyshymNSjPN##`(FIzigx%;VuOVu|RRlX5 z(G<_y0c0n4IX9$GaF`7)M zYll+-s|}syW$wH8e*XQx|IhqC{>T5GBJbk;n0D4*!auYUtkn$qy%f~q#4kdaX2vh% zNmOz6YH$+}Pi9m*@WyCMT{c6IU&=d?u#FTjbyD{>rR1reEB2fLLZyMCcu!rHgtAKG z&ExK8iX7ePpsY?{*~!(}KgWT^9XZ5Pc>M4YMs=X}P>;vF<`vhl{~I@P|IsyaTOh8a zTw6owjBIY6kY%l@?K3VBBsi28j8)VKS(cGy+4THE6etCs=+QtQtM-?Z+3enSqfokXCDwIv+|$ z>9j^&N5n3^{A*U>i}HkW8%s{7=-}!a&{R&? z*xZii`(mE)r?1e66hq9qQdX&Fyh2aKdiDSzfUBv7Biwi!nM8*qYjm`8R|ao$s@$ME z9g4X*D2mg9Nf>M5?rjVvFZjVv?&K%;{*>2UaT!~yN7=Vy2Os^#ckyrj{IA*E+N3kT z#P(=J$+1YP8uTG6qIt+O1d8bhyf`m2_z)R-c@{-O3Ds}gO#*qR;GX**;+x<4Hl6-} z8t}dbqj|-3*K+liH&U_?zxP|_$i8h&{Ew&;UF(s|$`52S6irt~+c=q^Pd zh$Tz)dE-^l97D@<@Kf!FQVJ(A8`5a9)S2ne4cNJB5BL1|4xW1C0J*hk@|5NaU-~j{ zdFz|7Cc~E%MyKH56B?SvGd5OZZ98}-g?Kh=y4gRE-WLujHwe`RS6_7_3rq71w~k?K zm#Q{AesB#;XUbD}5w%8pbS-!t(=nx9Vq$WF>$m_hjn)cZmyB1|a9bPbdO}AV3Z;>< zLL}B%K|HP$%HcNS$%tZc8QtwhdoR>;r}1gM(10QWD$B5)0$)$aiVnxNR`~d5|Bl_i z|J&>v447kBct5l+~40-1dmzBOdg?eGhQxi6^<}{Czly1gel^_08mS(tV5SR}YDl zHKj2BY+yL<&x4?)hH`^)>oBi+<(0haU2loLpcsrTc;d-59)Ds3OfTX&C&PClb**&5 zg5tIr{wp>syP^) zf$ny}*b}-SFT_Z+5&PJzAfI1gYq(8ebMnC=xBc`%{`6CyD)&{^8RH%@0lXB}2S6#Z7 zU;F4UG2buP*jmNpUA%Pp{;fY`b5x^?ep`CKnAL1t4U@H@=La-tlH;kzAY>->5mz%= zS;MWZQn-p-39`LT56_jC?BkMsdzl~1F`7(x{K!!rc>HlzSBA{Z?_fOKru3eCc_-QY z9HDtQXBAUZ=F-o&LqAvBw3Tiv2u6|5&Cwk%GFe*zm66Zw;G4JJNp5p~|05q{zEcpY z5x@GLcQY9D`Sdrx!Qs^-bO#G$*5bw$G8wmAb{7PtZG31HaZ;3SumaW2mjr2R%E*$7 zWS_e7tgfyThdL*VV{0ioMYL#7J7~l&rk^#7-NbSww7XT4ez!YNdo_}1r}3r~Fp2VL zl))#VT9e|4sA!y$u3imU-OeC^fI=r6!z1nV z?WUZWlCT=QX{3P=ksiyS|kTEDc! zlhbs_r+`#)FVaLSb(^JUQvFQOc>0=QB%|`2-p<`@)sE4|I=09_JYTx`hZIG@?|kT& z=qN)dM||iP-o*KPck<~kf0KJ2dW5R3vE6Qb0915+HfbKE3(;IwvK&zv;(dC^6Ejh! z;H~Hes0!1%+N!pmZd8w=;|{MH#$i%Bs?-G{y>!u_jW*?1>ALvj{@e(grX_wtxM%PS zl>$NhJ>TMRQ;{2O{ohc4wKmCEM;%9XiHuU8j$O4`LfhePIswVEA~XRn-9w#*$q z69rzP(lIrS<8?}RH%h@x8zdWY9Em<1LXxZ-h0QzBn?hT}d1~*OOeV=-MG{{nx_F5s z!+}cy<;6ND`Y|aPYbOV=OcmewcRX34-MrHhSwP#RXNiVU&>Q{DR zWc=+--(@l$^E)4YKg&fI8dF8c9G^7zL1m1B zV$%zC@MxW-moiy=rPi2A(V_wV;th(D-;iwZB90I_`PxKB>5vR}#i#P1=^P{v1l4#p zw5`XeUQ{xx+KaVwDo|2KBK^E_NJztgletLR-w_eKN{q=SBWqvqFl9p~+P1WQ0IKDW zs1VW!`ZRmAQPw7(O_ef@*3UtvFGzD=+W?g|45PEe!ivT+(mGeAF^i30t#R1WF%Sl& zu{LITQArz*!8@Ohg1lJd;RA>Ho6mfMBO4W2XJ@kcd*Rr_ zG^3Ba5RJoXA7%Z`bxVVuN#;JMYPA`J7MM&*{CG@93#zQJ^@I<;?R~uSwbwB@^fS|M`#pl$CN5mCxg)#E%4}O;R||Cio0WjRV3Sg9n*Z9 zx)?3osT4Nt;vr2$w^}9hSv_rkw!*CAc`zh=K-+|Sm{vYUliN+(myN8Q7FVEQgSOF6 zMHOmt!uAcK(SYTqF||>t#Av3sCvjZEvTW$?%A%Ad_&7Qs`b@;&8APze%)=-h0a70@ zX?Ez+!hrqzme{xBRn%@wRaFePMr;p9j7DQ7b2GlGXhq)N5rgPU04C#gP-GIwslnB#;8DTH<3};B6%`qx>`Rw?Jj=4G@dgy3 zo{Uko1M#?W%!P~dyzOuYq1 z4w=<_{kEU5zPZKke&mB(wr?l(_7-JX(#bQ<-?`iZZb1aCGfJ;xCDcS@C*i)!|BY35AWWamFU=PBT( z+O)ITbz)aTwVDN$sL&>58sa@h8!~OMnL&b!HLZ$0h=@L$)~u!Z+CVhftwrN~h0QF^ zhvZL?%x;)sOwx3Fq$!0oO`BvEW1+=Tl0l45=chtajy96kX-U)sA4$Pfspy8ILZe-X zpk_3m@hy4F6GtON*>pf_g{y@;({zh2TA*?@A=G4rV!0TwbJqg46Q9S-SiG;;++M}# zjDBYxZTtN2w!8WK7r)Qywk8`a$4uv?1c&r{Mm!lMOjcRy=PdSnC?(O|IzTOgQ$=(y z5Au?}^Vyb#)bjEUnT(@ANrU)`{d>-1&+-B?T*ZiQp1YP!sFfyHLS0U<)rj}q_!=HR zaDdhAG2{io#Ib5x8MKXoMQe-6GDel>sg+}_Y;C4`RWaGvU{Y6f78WtxZcK-t50_|W zt#mu|_wFJ{pj=-^Ysk9u{OEy)S^d+$_xA@L4;t7lLqbr&>P4WsQXwl}vJ4TpH|i87Bm8M0)N=&fUE zVXURo>#$?@PO?syP#e5!-7&FAb*d!|=E37bEHy&g5s8;Yn%YGqbw(N@SOgen?{bqt zMN!vGC!nRKD?~?UZq-=trC`!XAfAEckOmPYjf=4$j>jH5$oyiDPT$h$Eh5UHqz2z; zNjxDqP(qgVu&T>&togw$xA4_({g83pC+jV>{ueLBrAkE51adV&Z5`r*#e&~>|2x^e zG>3EB7}LQA!>EkG@TJaud9IZ#p@{(^rChi;i(p|eAUB%eYK&5G+?#k@Ri#=wBg=C- zI%XgH7UuZfk9>$y6rxS^gG{NoZt#U@por~tIe2WH&wl;ueE-g$V&|7B@;+g+W>Qxa z%R9*Wef$ZWgleWf}6sz#;eB&qUp?A9z3$jzy6C)al;C`Dr>pxk;f^+6drDNJl|4=X1g)dKMNrZc4;{Rjg~^Ayaz2UgOOwd-*{R) z9mH$W>Xv1wOq)iENUIcVbPK8>I;9yKEj>Q9MV*VPMvdmsQwMqA?t9r-Uk8aUTp_sj zlr%Q+{zD7u83Vib@8g>5uOsjEnE2=(rPBaqqfPAmq?c4{X=%njsXNgMFFux4$r`Zr ze9#G;Y*?L*QA&%1NPCT&6_2}OyZ7v1*YX^T3qAVXPTJt2i*{8B_Z~RH%{Sl9!;c69WKdwhywr7!l z^08lI_tMw->JM+hBXm1`MxzZ%z!;6`cH?xV=Mq=VOuo{c4xQb*AuFiXSE#nP=nWPb zyPBJRaxXXC@?#2Pu~sFpF3nIBjdKB~G$oo^caXxi!%E^ypL`6Iq)AduoCqx_Nz)#b z#y@guz|u&)P1Bbr37!-!VJQaDkY0K{8 zwXbcwEIKhPQzp<15%xZ5P7;wzYn1Y-Ow-fONJ>E(Ohh%fiHUE2XjE4;*E?Y&nwI(5 zWWBMilg10g>7?XY2i@JlSOSkcHR8bo52Co2xq)SAzR$wq91DvJ7}Et{b!DAL9zVvU zR@mO|ICy#H*?ci+eiK-X1PUtj@E{k>1%B)Ay^GggeIcXm6-0SDy?M4LhA({cRvtY% zLKQn(3&2ysVs3^?#0SD;U`%WYY0Kf2HMYubhLCq+T(n&87 zf7fW!`W#qm+W;bsexU_9XK33d@;Z#M496ozTSKfhbZv&z6$FPxaNb2>x$=157|9xP zn^A`tAjjiznxLhUuB^#;5@#ZHLv#XkT>})RoWGGN2WGS&>8+NTc_wYALz?MfKCwb+ zyGW84p>ei|wAq!YkeVnlyNdW%+?kDen#|s$=RWITti=@ZoPw(v*Aupm*2s|+R9HdM zTn>Z5b_Qf!+jbCMO8wuow2cG`Rbsafa=~2Sw?6WAUUSt2RKqn~IU(;3@SSD8^o`s3 z)*X-Hi`|5vTBPWd1&F8!wy~>VB2V4ei~~oGamND>@%|gGVLaTzO3Vms)Ml`PvV^!sovAUGn}y8VQzU*(lHklhkg#EaTcMuHc<-c|D80j4n zU;Y{+ADHXUu|3>otPNeWyVZF-FHfS`)s4mu2F&F-<>of!)&}Kpgj98;^qQh&j=EN= zoTfPi!9_eVdKe^9kF@>=Q@3T2SRtDkD}HH9gcKwa15PJ-bRYsvibl5H^cj*2T;k7$ zkW3;Hl}-rwpjyw$5JFO5>V)9aem`w#G?gVQ0av@$DH~s-+=Nh$@KTYR6x>8dpv@R0 z>!x6k+@@V<&p*hteLB?|B%h2_DumSLw%Xu*;~6n!UdAk<;ZDpvBA<$(dvV!z z=QD<^+hw9-THP(WY;TSE%vW#XwO3!o()=#U&0{$4u_^Vdm7&y@Vqux@+<6~=_q|&= zQY&O@LM;*bZe|9?)?6C&>TB*kaDYQ=$M}s8y`RNSfgf)2%kO$CM#0Cw^fg>*n9KWY zjYpIl>lDj7Ah*vufMgcoM=K}_isg)AZa_U8;l^Wp?HVc}K1j4Cm$(Zm9}wq|vP{fg zHO+$bO#3m(aB1{Qi9*dxyGfHg0}U7hA|}=NjdCHqjHEVcgWol7&FMXE71G3%O>0Xa zQiZL`QsxlwE^!dllxNWH#5xA*$#8^(K&C+I5%UFn_=E4`;{7|2YK%1&=OOFP@#CL9 z$mhTIU7RRP(L;+6yo-Z>18MGQ5>sj2eqUrvs@4uq33wMBophqLN0Lu7nji&^MxLGQ z{2L~yYJw%c6)MFgDP!?1OSI9XsKm{X8B^)_vyBcf9+#GQ?PtMWo}QIT^ZO0|^=T4; z7x{M1;!e4)u-iwtcv1MhUwJpLzH&d+Xe~}b@69vx9scHvKjKS2e2}et7hZK^7Iuo& z)3J+@zQ7h;@@|K6vn01U#bAja-TM&#_TzuUfBT!i%JS~>alA=SZYxQS{6gwp=LQltSnjkybLimr&CrDrHn(SITB zxJl;WeT6lKBFiAe`ad&qNA&pEI&9j}$jccRz%@Rp8mef6+>2*9$ z7viP9l+)l4$aKJr*4f*g@H-!gnZtN<1@Ls{mslTV{M8q3=IcLxjB&9WZ@Y;^cIw=$ zn`K)@9fB@$vc*M0Sy9y`+T_^o9H0O0k2tcr#z)`#PA)%xAHCiHSHr<0>)iCCZ}OGz zeV?P_5_(Haf}vE3U~^O_G9|UOad*`UDN81k5vrch9W3+RJMIJZX@2jcA7odrpdN4X z{*2WHcIa;UyQdv~x!!93_2(3aRu-1SM+>byX*!l%mkW@_a!*j4>Nq zn8|u<%X*DAp3HAisETb5nVwFyibS25NuYIXp-K#*HceXhlaj-Sk1!sM*u8r{yP|0*Oo|-pxcZ zTAJ6-K0l{wHJO7|f*!52yO{7hAAUElzTyJP;W|Ntth2kFbBoGK&lIlvT~4l{JoSZ6lo? zYJQ12XvRttIvuk4Me@M_)9s|{S`x_Q@xu}0)uU9SZSub6+dsY=6#T)jew3~OzrDdP zzvt~_HseqK?hA+#`kgtpH#ex2!p<*1mOVodc$ORS0#{Hf0dz694+$@5Kd)sMrU$Tb zlo$Q9YEW9)bK{3es?YV)WW^y(({5e|uckC;678sVLWXA0?XteI#yvl|gJVaJ0kD2- zh4U}Ih>I?}gkHbTxT2foh+vujk)iRyEypx6NC_WYVa5rUv?Q6UUo?Y zh--&V1AAI(L%J4PCmMR=xhbU5G_@=n>b3~t(muS=7=Fg32fZIAc(~9^K`g zWP<^u8nwAaA@Tgp+&sO^GC6z%S9=zgcJsYE@57kS@;e{-0871`@y042c*mPT2_OH$ zm)Wj8{ka9UH&$>Vkj*WkiYyYGW{5G*r3igNtC`wm49XfpvzyZp`lcXN5W%**_~uWW zS(?XDbmEYeX5`+Ct5QhqO4aZJn#0gK+4;u+TjV)MpE}4rKe>yIwN(n6W2|PfwZ;8+ z-NnYr3YTAXCClxUaiPd_s?nHx@3@PNW5>viB9j_L$#TD-XFyNJWZK2q8U}Qj(9?mA z^4Ji~a&t6WE9*Rb|NYD@FVY(fD0-bZ6H^?SwG1~lx$myKIsC*^SQMGfm~3ux-yL@` z8IQQ+%FF5H9VV_$c6zb%3~@6K)f4NtAa7LhJE)s@H5;VF_gACJTO*UwhvhcM28jGWAJRR+^p+@^U zR$*RYLq*>TnV(>sLi>_I)?qRka^D?y(4FhEW7kd=mX_#t`waR6hUGT*-F-KQo_vyi zk;iPvI|f;7lRo;>2M`~){F-aXGtHzfBNkvx3;czqMVu6@@iOSvU`Ik@wwt;{N?HRP zrB$oFP^r7&rA~NJlQGPcFG{taBXVlX`DvCLGl#t1X5f$V!M9$=J750_)MSIYc666^ zaqr{n{L!bs#UsZ8GT2QW>`T^myx4V|qtt8%z;rtF@(kbWDnaZFod*@Z&# z0^947^?R72K&+XOcK_@Nwe=s6%@0s|FO$QEFeP;66<`0+Z3H~O`^z7quQlWKRX+H( zH`B=r{_2ZgWn;L_e0Pq`jWz1pMNNOd2gcIolP~omYDoiP8+mbK0^2k{Bu(a?roK)4 ztl6?ui+X!RW&|4o zzM9}Akau!~jGoD;hTHt~_8)Wb(Z{d`w$|5i<%B_zp+kk9l=$s+7L4Pn3wD!*3BDf3 z(!&RgSA@}!3wJN__Sap{r@wJ4TU$pkodx@81bAu-YicXhKr$bqmRKpST znZZbf3L&;OO%@x$+VJT84^U1fTzkW->E#{9b(v;wO6&dstUb!a zmTpsqHKiORp%y9*@JXPmmKpy>iDg~npf#J>spBj+txSu@R$KI{BYf!X*Ym-5zMfF7 zLhy9vcXG!A2l+3b`ymgkE2O)NI(4~T3U&LXte^3uoE1rlHrPQA+v^hM7YNR^7g8lk zuhIsc<%pTePMZ^YHt?{a1fw7u%%QwzeCQy-J374uZo2hOI(fmb{nC4w%M4+-#d~jj zJ-M-b{0m=ZeYn9~(Puo~VsfNJ26L#Qz}Ot}9IRzJR%!;&d8Xr;Dw5+dOEk?<&ZER5 z-Xx!fSW5WTFeXf`(b8EkV?5Giy?L^{ z2U&+n9jMC@qv0k>!JM_IP~pbgxN1VcQ1s@|x`WONL>Ueqc$_THx$62?P#8;p=HEvV*0f(9UwC91IHIC(sxN2^XN^{bG@Z$4gEgAR4;>4B{@FkNeReJ{^2dMrXWV-GUD!^IuQK{sj?Pu|d2l0SGQ@;2 z*IvAr55MbZCMIdIWRrTh$=)6Fyzf^&%=*@d&wu4xl;SX2k)ctU;jsrF z#0SUKue^>-TgtkM*FY&;nt~oj=S^psh8e}8q(G+I`=Htls^P9^tx?*ylfrDnJ!u%4 zLDH?0s5tiN;ZNPAmKJwl-K()c7 zuIVrB|%3_hXo4ZE?KUe!)yO6p|!SX!+=%XLxZLhz9dmlc)p<^qoZErIkS4>=u3xUe}sL)Ji^gJXr==O2^x&!V(6+NA(!<)$muNtG~jSbJ-u z8)ye(4CCRDhaY&5dN`(=LLr(v>*-`K+WicEQOCD<%YxmHPk5d@oAtODmI zAQiGb!eN=u=XmS2=kuoPFX#S85AwYq-Ok*4;LTV7 zfPl3IS5F<)o9rS2lno8skZOx|YL%(SbgFfV+@oX?rTb|nvQ~`g=NWjVmfXh z&RUHLgu|k-iFuo8h1G%OxgIyZ`YK-gs;lwkHsiXYH@AZ$+nT@l?6>&IW1EED-WmGz z%UfDB>x>!RD+-*^0NE5vlEjnNS_r!7sx=NcX;y;}fwqKX_zi;KG*?h= zt`OFTSYKg+!(bzS6$4~)!46WReF?rs$}KQC*Pl1XD}VJptZv-M zq@1v{FozMrl}8X)5xihKb5t8g_@#HfncMGwgin3x+jR0SIw3ptq-H!G(-{onxf6XY z8bGquQmCYZX`Yv)3`Iu|3e`Gm#Y@}}#F_xnl)bSv8l$jQg29o6lJ~voja+*1B2tgg z%3-aJ+9Q(xr3M{L3=WBf8Rw-YKw3xEs5X&QXHe)|lW9efWekd(POkB(TT%3vIJ_;+!rra=^Y;h5>2DiRWHQquLeEqh&Slisv_qYTe=yjaH)pGHD;ySf~1CW<;Y=X)If3I1)vk)9G{= zjUqi-DTOu}KmN&&S$W5+Ij?Jx$%LK}ERJeCCXBZjT>R!AXsh=#GU5oh&B=M^#VI&JyY&UK!?X!9o}O@EEQxF($o; zHfK~<=v;X0z~ek{{{!S%fmKln?0ldzn4{b4r}FAJ)ORWaH_^4a88AEx0Ji{fLtK;D zxgZI2*3;4hnk-@(z%w1V>e79@@|t~w$vRfoWF|%M5Zcb2q~IOlBps#9O*xdjAscr> z@59uxR|0i4WK>0a#pU7tFL75&Lz5em)t1wOs961V6zwiLdqJ9HYgE%RVCBY z#t|v`9n=*{6jo)_-cjTQi*s`r1x8y$RgqrY>_8@UM`i%C=*lIntvl(#Y<&mBw~bGo zu&mI`Q)|Far4SWI*44GAJLq%%`4@2C%3ahxN}Ri$F1Ou&Cx8AIpWt8o-bWd9=TPGn z@HK_wE2EPC^v8eAgAYAMvA7o(0+X_WEa&p8 zuAn#QGpfqyLD}LDaWxtbYjkv=N={OwVYbl*K&Lq)f@!dY8D^%^je<1YKb-=xF{luK zx14M<8Ev8`8+fUxBZQvH5}oFlrl6z)Dx@Jk?Q?6VCN)hrCMjz*(e5Dc3<##j9S2Q)U_4>uYqfoX55|IQX3(@|`>G zVdugeOY?K=T3BRZVUAA6P~-)jPKPYB7#&Sbv&>?QjgytEi4M{;si)G(EXJ3pqRV6y zIJ&W!xOp=@iH08Cuuhxf)`?23r(_(}QmdoKgK1SC@tAy~V8>2M8fKg7>egaSDfaB! z$B~1FSU-9UQ3{X7=3PGj)tk8PqTT$xcifQV-5wkatK-+YPh-+Cwgg?&s?mm~5A&nKE znwstEW*3+CJ{YZ|B~0tAoeX6(C=&yXD?OqN{e@+E^E=SJ41$Y^;0(v5&GA&kkO($@ zpPJ74Xl8~OZCWO$peUVUTm@DRj=Afn4{-CH5A)PeA-%mgvY(-wpGoVC3J^RkNJJaC zoP5V_Y`;&vwFOxhDaSlfPI%&pRg7ARf~X)EEo~ZaOni7UqY^rAlixrT7tONjlr|`d z0HH%sie`0ljO`8Z$qh+qjWJWd$>WYy8Zk!n6=2+l#}gzXa6 z#-5FlP?-Vr2YB?$&U1Lab;bntLKes>_k5E;Gt1F}~sj4c1SuVZIoBd?qnjUiAIvSu4)~kQYtwUgl3MIU{sP9>u89o3|g6~ z*MRcTx1jY+h~KHgl(E?mf17z$#u#b|4Cd##{K_l2`}W)Lb%{gBO~Jt!TX4Or&?~#u~0h9~qH7 zaD*>@>rVFW_!e)t?ke8(*4Of?E6=B!b@0PACT>iw0$FAWQd3uHinj^`B?&LNo8j1_ z<}F?+T=4ALdmiUqd;wJmxX^m?MnJa?$-i_OMAO5NMhs4`y_%V|skO!cLC^X^rk|m) z;fsiiR!yfvqfCagmapA%A11ev(b`NVOF=0#%)tg#Xo01eX0*ec6I-$&W}%uw!{SVi z=pJ-)P}wy5Jac0MoT&lgnU(-zW2un!dISk%&f}|!P?l-NVw3_m?M5v=(tXQ%T0m42)3W1ud$RQCt27@o zJ#Zm-tT8EQL0#8ale2T@PI~anRWA02wS1Fo;eAdmCR(0tAP1HIJ@W9QfL;eDg;?;e&6#kzfDN zJJ>teO}TzB(!%lR5HSceID7ZvxN{ks=WICew*!N>V&KYi}%%d}ny5c-?}q7^+o(`TJRqs(VpjSea_H$FGmEJu2M z@`jH;i}Qq(0(u87brn?PKs0VVjUg>zMv3VHlg(|ycocVr;N$1a5awvqmukjlz0Cp= zRi-Irng`6-^nD?w&`D*e)ijN^iHc2#rG`?fwMR3_OTsyasw?J}`s`d@jAkz4BhsHb zC;^4j7H&_`A+`rpM91`kBu>d1(fYlil+8~+QpaGhpF+dRuaKqRgmXh^3EKG zCfoe+XTQr&AAN%V<~QEYt1jA$-#$tmM%WNdaV0o3hJZurlFKi>fXx1gv9Hml3tBU& zDqQW*Dq8e6EkhL~jn#{N@8j*; zKj<(ha$GrT`~HnsY3vo1E--^#eEGX~@UQ;((;OWuvZY;As$k+_Tvdp+^ycT79}LK{ z9Gw~J+A$svag#Bl;fQhV>1H`aF%LH9rk@;Oxcv$K&;Rh(xc-7ACc}!{Pss7))}(QE zB~p#J{Nf8(=yzBjj85+duO)GK`2t>i5LvVX^|iiFoTCf&2+$+zQ}eVODQ3(_7fWFP}Zlzx<#7hX31t`v+XPYa4Fe;N1l448lvagsPdcb8!bZ zTzMHU-uEN|OQ9v}t81J&b((FvcOnWAvNtKBoxQ_4i<1iDY#6Ubm2Vd(6uDP%Y-ywR zWR%3GQ?MqMOKA!bG87p{ojPP431u0Z#clEUDjCFU{#(}|d5vGV@JGj|@(~;sQ7_Te z-Jx0pvG@(;kA9^W#Cw$Rff{V4;A*h8RXUO~$Ey;rN{rC28i6k4@rps|+?4PB% zKZ2rFT(mOzQqWFFJh5ZW4Ca0&8U?(BLMVk-!8J<8#-)o|4~elUeVFr0pq|uJO+y_i z$m6X=jckspMUi(4xgQ%7nc|r z$;cRr;Ud@GcmtPSaRs_8aR_Q_1E95X`6QFc zh^*|zQFCbWuZDo#BeVi__SwC1koCDU@kLsv8sC&pb_3`p$X<*WG+GJ9g~g=lA@SjkRUw^B#i*`0mdh z<@zi4^QGIb!&MV1S5as|CIq#qFw=FezHBcCc5mmA<16Tel75~u9j|ln$tOVSV5k%v ze2_}9eb+AbUa~(J3!5BY=_6(SftrBt`|81=rLa(N$wXPkvh zzGIvGTYAhs*aDC8234(*n^ktr39i0m7dy8upryxp&*|k24!(GjqsuF7n0Y7`up-~& zK@)s2(yZ89N;g7H&T#Rf; zNc2lrv=BBb?=ZdvA4EWEbTCBC=7 z^T$^C&QBhsQU!*b$|}m?9Jk)_3AXRq#ki@dT>v;K9p(6iK=>>)J7pAVGXYB>fE^cK z#0PKv2oL`3XG}+Hc-MgbL8-gl>LGoyLUxggafZ)`Je(fB^vqQT;g8gO>F+!E*PlT zqVQgJB?MijJlm{$yVvVmd35-W;QOjYjh?KrRj=}?+pgdfAHJHUfufWS?+sFEEQ;lc zPz^~Kl~-%{OOadd#Lr0CXS-~JD5eh>G!oZLXMfSdy_LeR75K{iFcD65;1Z}&d}O@P zfsMw)BowF#(C@^8Ovpf?LVa2{4YhOZ*}tFF<+D8U!2L`ulx2@c4xZ$J#}4wvTd$(7 z))9V65vlCTR%Ei_wi~bHyZ8K@Gt&)ZsZlD&3q?b~nKf8crm+G>RmIxbWp?h`6)gIk z!<#hX3?M2Y1U4ZsZc>w1nCkR9>>Zoa1vJxq_=N-_M_a?I-;F*;Dx5E^1NW zlkB~~li|?Q6|TByz<>SwpJDIToO<~%!cGI@jbPQTYM`;0={gGw+xSPHzKQY120#48 z3si&MR3gWcVTEqfx>rJjyk;9*VD^$>5^6s*lnV(ZR_j=g6!@F4La6)osZNfbd{F+86gpgFVVcV`<9C+WQ)W)=* z6%hlNvr#zHv{M?N^eMs@0YqgjwKZJ4{{Tl09pdcK!zh&_{Fr-x{TO%Ld=+_CV(Sr3 zIFt{X9A>=Am3y~x$4yuBw|D&t?LD;*uyJ%j94H+9Dp|8wx72>MYyJ5r6c#+xhS{yKoyPz)Z+}0|af^%r;2l zVPdH32?v&T@=w2X8|(k+$Nc)}GP=KmMyS+-5J+1yuWLSg`>pKXK4h|d2x+Ed!jWlB zuF7EZhC@@)ILmbP6lLG=TOYfQb;a{P9pb-xM@Ux01sNzeRz!D3I7c|$Mr zEDZ`S-?g33fBY6M-L-&QJBIKzURj(4D-=5K@!|`oIdtp{vM6a#RIX;vCHqlD$*8J= zIgiTl-UMoS3SeHv&*9rzU&gXhN&!kSt!s*YpY6MMapuHPoK{$!@$?JFIC^TCy>k*_ z1t=F7v!=$IHHKNA&wb=tjvYP0vDFPk(Wi2fTI3knwJ#PmQLhsqLI zEvZ38F6j@jwp+;$+@wC~r{U|@< z$vQh0o!NXk7(N}tv|YPmhUqCcJ^Uj1dTa_ZsVtg#l9DZ9d>!c=nkiYmM%k?J#V_5) zt=I3ztRF*}F~S>)qDPixL4TxhRMT+~!|^qf<>TzzelcJA*atZFclU6tT1S=(QT872 z)r2cA-^G=e?4sT{h4xc&eY`UYE;=D75deUv?onNG2jW%g~^#iwt% z3NyN&jp|Ie@ixP_M09CCebjRF-NQ>{&-j2Ja8a#ErjcQ*9wgcYD@90$WwfRz3Y!ZN z6`~@`PDV{b8C|A?5MW*4I?T?clkJ*Nc&`}?OKmnVk-jTPR&pJ8!h2dT$kdkzLMN&# zaYQFxk`|NbE(1u3(wmuwxEg6|KKrTL_{>LcU?`^aM9n}s<_8%InIN|nZhQ*utFZe= zR*{l9Dl2*J(34Cm@OeSwK$j)kcJ4-H1=g4d!w!o#u^+=_jwr<#ACj?gl4bMW(O6He z1xwp^kQW0?U6ExyPOpwRbmTY(uG4((QBncmMqJtWF(Pl~i71 z^c-W+<1fGdL+*L_8M3lZomPeq%{1>F-X`;$$kg^8OQ)ZjfN4@nnrg~uyg^l0!Kq$& ztPB34jco!IJ8nqATeO`p@5kg)ft#X46A+gXNarHF-nyBMW+rPzNSrmTK1GKENa4b^ zj^lydANfH<$--#P6hS=^4Cs)qQ)+iJB0~<(Ot=hFM?8f#2)oYZ`?m6t>-M7UDz;gt zC^VTGu(~?s*{7eOsw(#GyNCn(cD2$4rmFE9%iM6qZm!+8jZ;sZ#T7$zn2kCzDlWNb zKa2A@W^F|74LZwMUS8+!pZuKXUpR_w9C_v$-7?~)o3Ei?1T8_cc8Z&?*vq~hORP;S zle!6NO|mnN5i*ReAc~W;3PH5kbs4z;;b9jomfZ;-ydYy;+v)~J+<$m3W!I>Q7fnPL zEftACBAU?3qy!Y@(3O-};W17ivU!f5sri5Z+rMRFV?t5(SzU4+~?!JJTNH%0jhJKaFk6-rbnVG%N9 zSJpYa4vuC=PM)H&9xp@o(<=we&CP)d8y&UGF7Co2?yx?pVu`jSm&|MBBdqEvd~<$4&h*Tzt2TWLnq;G{_@L=i8dc%atA(ZZTG0`H?OQ;&d%JLfZWSw4_Q@XW>;aQWar!!lQq7c`Q4TL? zkXUkFBJyhW?9zz-ch z!jn(G0HQ~xOD5HbXAd6Y>T51%IG2-2N8>6=DcHGKuytM_l#VF1sCf`)&`$Wsn)FEJ zV_ldoPHVB+rR;PJz676bm4+f8*I@&SFt)3cz9S<1o1|+#Nhw2vO9k;-;nO1zXi4or zWPQq_&#5xISI?a$<=!I~tk#6#u6v>4$+R{7kV#6?&JIR!~1WxmMCb>7+pD5+Xc z%k;3N)VCWKrsXf%d*ox_Gsd8`Cd)Hw98xRl+OxJkp|Ra-3Mr7zBAh1|3U9~Q(G)XY z!J$AGn5@QVi>*fJlsQHX2-!mIHTpXjSwlofznu>m= zvkmLy*49znQ$y9+4s@4(&c(7rMr(l1e$V1v@C=c76b>yMm@%0QTCGxQ4nBW~M;?6^ zFNR2&ab|giAK!gHd-v{U$MyvpJH?qA;VO1)ohQ%21`>M^pa)L5K%Q<3htw;mI?B5Ekp2XrvSR2S^j}5sC8Yec)fk zeZ*SRZYDwo>%GP_SUh#pV4XpUki9m}qVj%A0s|klGCY_D>@+DO z@l2c#pg!*r-eIuhDuVfa5cgYN*}ynKZ`emGjrWH2wK3~!BercFBBe)0Swy9E40s-m z_c*DUP9~0 z$XG}+VIrdHe6(Y8RzNlnnZ=QXtiUJoP*6K=y*69WWf$yZlsFfy)@7oWr$7=2UdDMc zEhF7Lrp^QbT_1ZFUT6%ONgcEdavxNqm9Q8eOmanHr`oWJmD0#8Lo1DS5fv%GdK*&a zC|=V(*xrE`sMr(?I*%PXk;pAEK@UFM2kViUYIiYEcvq7N$g`{!>vfLsfQvX$K%!J0 zNMugXAelIeC~}+^RE=r-H$kp#bAKl|hj+arYYaA$y3+oVd^rUQV{O2jc!716W;CL) zj-oeDm#~VUQ7SYt8(?LL${b}ch|dPzhin_uvMpu0^O5mrv1FAJI1zWc+P!v#2 zR%uj5Q;j%q*)FzjTSN$lwFZYmO4t~U!snBMppzvOH5o6QIL*e?ve=tvx;8?~iUXHi z%$;}M#?wzfhcSjr_wVJpYc5CNs2YPoBg+MzICPpvpE(*-d1Zzbp`P_bIrI3Ttw{i#<f$gUHIr|DWmX$A^eCP;2L;GrMccCKO%0iEXLjVPU$+yHH_5#yL2u#HIut`m?|}1 zQh7<`G@_hCXFaB>kI#lQUeh>FE;Yo4pAssx=0wo1RIw@Stff^1Yt{M$n$WaP=t^rW zQiV0(ybC0>@f2rlB*}r!N>FGYsPH38n{rh(FiI3jQBab|Tb&ef75AdlwKEiOmM`fo}7Q9xVa}FOr!>BUI zVHQdZ=Rsv92M?d((9u;E4-5kxxUMOr<@)Qd=IX01#~I6TZoqWB&S*LfbM|tP4d3Uk zpFhF9j~#+y3l*Uph*L#OtM;RCKFNTk55F0!kl1^zAgSm`O+*G~*E5u@=3|07clA$r z-J}N4PK)BZ1i9Am`(8rh3!E$vUSgYu4XZ&*917d+xPC@#CW^jLli0LU6P`sJTD*$kX(+U`|wQEhSrfnuUD8(%d}FbcJ!fO71ZRus2#knD-nWDUn&HMmz(pqA?WW!5jZ;7n8z{nMB7?_xX4+Bhyvhw>e& zId#KB9vsKbGpVageVI0E?Itq2=X3a-k`G;XfO_Qwb~L5mEBV;%H*?LE@54FA!j^gF z204?-I>tJjERp3LM^0_<)C;GmRWE3YwmU@L=hVguKfd=7F2DE=2E*-`_0vJP)Q!os zLMXx7#u~<1tjLl5MNF~8cYpX04?e#}Gj|D9K9BLbZQOJpO3bi7lM{2dKQS9B&iRKR zE#55(VrC*|8a!dH9DfEm<1Mf${=70mO30u;=~Z~JZ3QETF=LePCLmz}gw$L`^qrHb zS>$oT234Wd*h>y&ab(A==cJ6Hy2;#Egpr<>5}Eh;;m>}-0}njFTp?J@B=ebP*Fujw zKYSw}z2P!4<+0OcluUK5kJ9uFm+rlYVGiS_LZ}jFYfhdx%AQO2l4m)M3uTFqSzbsS zqU%_wF}nVaOA9^(Hmx;y$C;BS5zeBuN7$Mj+veD_b348sg>FkbWgzHIh#s=PmFLe) zxbG+T@!auMRwfM_R&sXYIkDcL%Q>8n;=>{Zc^k6?No?1I2}!Fa9zEklYUFu@2w9Bt z4q-$8%2BH#PjeLQ<&4CQVNB0#M@&>&Iy68Am+65H;&pWpuU zhnQdNpFP3Xe)be+<#tT5K!+z!m+H7Id&x=+jh700HcyXV2J zBJ2c_?MX7vAonz!s~X}loM%P2@2sJC8ccuLlqig9B&UXM`CgshqJ)U$`M z=1Y9`mMg(E*k&W7f(vZ_91IkhYhY!8T)$hNaeAuTpCZx`hEaoGB zMnpeL5fz$3BiNyo#5NXdJ;Pqf%JJizK5>$Mp-@eQYAUYUcNtp;IgC#uoDCFsJWeRa zMv^UU=lK(3{$GFoZ61E^6cYhPYpm*FM1d%W=pw^K6s(K>rq0GJ&xJtdQqMs}17uue zphnt#d~{sOPKEFKVAj&q70!n2Q3)_Y;H0LJ8A2y(N&N=3;QZztXwy2|v;tG7;y17L z-|%m2dWSB602mR3f3jiDvn$8>vu`|r`{Hd}f5pYfW&<!Kl z`R>mi;|CAEz%<)}Q6(S()H-GCO;)nuxAFC#J;RB!BR>DJo4I87PRi0EoC!cCAutA> zKfT5e?z@}2A34Msv6UuUz<6~Ivie-@MrNi4uk`<3esRwI`ZvAm%YTYo@sAIjFu zl|43AS2_69Gu-gu4^o*Xj?#h-gYW{|gb9RBdKNBfI7%H-edmKl#iZiJ=U>1~M)VX= zZLlSWTW`3UoC(_1NN14}>Lw^#%iaQ~CxSo!`rSP6^eNQBJmkZmL!e6(5+fuN>#(-Q zc)^ajVHjQenL8w9C=FC7U6h)^B#9uhBV-b_jU#a>g4Si6TA0SPX>CjX5?ycTRhQFi z9^#z8XTKD$u%^YiKmDdxwv`$$2k3>pJiUB|fAcp#;m!|T!EHBP#g4^`&^nOglu$UK zsT#@Q!z%i}17OmM*d25k3mk z&IFK@u7|QCS;d82(K>@`pvZIHch!|-S;orQv()1;&e+)Si@>4SFBuHB@cfC>{QdWT z%I@F)Ec3cR`6?va*3e8gQ2CILeds#AefKXoQIE+71sR?vAA6K-7wu%nMZ2j8{10I} z4U5pQO|1ukB{+#Sbx6s}Jm>JU&+^i9&ru2}T?5sW58kk!_w89i)oVy!2R?~V7%Q;4 zKn}L>;Dax6_ruR47v^ZPd8Q&`>cN=U>rfFPs5FB?pZSG_c&%bT#KjYf*Z=_M1Wmd& zvFyZmqP4!w}q;$YV97i-uadC{A#)I9O6y11H zN`w|PrlIKdx$?SesizZ0>mzK_fb(QhvazzvOV2*TWTTn%eGfg!XK%lS4_>x} zZKfDoA@OA1;F~emUV0H<`1ptT({KC?vVvhYV0FB~{rB9%C+@tH`K?RXsiiRi(V6rs zkOJF;Mx?VA?*+XqM+wiVqepn^k%v*vQVNS(U**73&hLKuHu|DMOg0du1hz&kujdjW^%Ip8XeNJWQ%4oRf`A$5;sFTB3!} z@WtD2zg$k9optMyWiJVpw(@7l@E{rk~H5t0KT z5Gunv6OC~^#yK*d%!_1aXXwKHNkX?+@OB317XYmBex*rMqKMm86qqz!i=B%&kb31t z_G=yQvS*+riwTKSC9W)z2*aipKpL64-yk3PG9kLz9}2%1V8l_}982u@$C&+22Zywq zcYhL3#xuLex86wS{`@QayNJryVrB{vR$&YYarDqaY?=MtZtNz)tfQ?-d|dQi1frRY z+zqL9)Ch#ySd0r`sM=XZ(+cA(yLMf~aB&HP#H$P=Gah^Dd6qXOI5f_I)+#`4ZB4y$ zntfXb{PAzy$*zHf^<{ckmWCy3r%!R;-S_bDgAcKBcA4CJ7RrLTvSd)?42pvJqM(qH zjWcI>{FlGvmp{7~v$4*Wo<>*esOdT%yYUh}cGESe=|*tKjt^~P9Cft!^QzAy&m3Z8 z1tKpoULbVF_T4+_FU(@NfFK>|kH85T zH7Q^DS^kZpwOwuY8{YjLl$M*l$iMOZU-s`}(_b(D`I)a>Ku)G06Oe_8d_#w~h)T*{ z%}h%Y(I^_Tr0?-T&j6WV*fMfZT_gl0zj?wg=X?Z1c^YHs4fH0c_@Dvi{bc9%6;nwRe=AV4|Q!HwS8?Vw6 z4&hx3inHD$bk6Cu2~Qk6gwHZ6FR86%%a%p5evk2V8c>?i4lMZm=oVaN0U6-(%G4u8XSkURA`$VrwItecYRdkIoD;Odt1Pl&yPenz z+TYuhp1h66n|YSsGtOG!@KF>bpvGJjun~3|il5}Qkl-xsU5;6YjA%g-=H1fQ5Ph6I z5-&EZecHCWX_6kRU+a&%;AZNvt*SXeV%O|;Z zz|;oiJw=hTeDV~hjvXi0S*x(!SWDA1Aq|hBJGds0?R1VeQ<{-QIm;Jry@6fZw(=L> zyo-mPc?m0m+jO3n0UGZljSJUQ5&}||DO@!{j%&nEuiAXh=4CDW`euQAyD9c$vT%N$_XqY-}dd@*SKH zLBLtpxqr<#l6mKFvfB!rvq9Wec8y9Y^`^C8jREJ-&g0IWAqtH~)$!C{3wKZHEM|i) zOidyj8iJvbQwp*SpS(mxz-%Cclz|fxnJIKxB8wv2tM`D!-FqTHr0hWX25m>U$r_n* zXz9pggYpf68YxwaF9@@I8~;tNZ}}pl6rl_!6%+z zytcwH(@5VSond}hvM@KmRMQYR1tAr89^pdyqV**|kWD$=Lsh&8~fT`$V7-o_yuDqB#Z~Fi@U9p#*ALBR9pqnvLdbEt%5%FO$ z&S9j%nJSbCE_BS5&`4`pTUp`wk)!P1w-*@=kwYL*B;mMJM7op>MvV%oRK%BrGRAll z{4#_*y0`K_~|y(wc65O+$O$WM{yAx#6-2;m!~7f2mg$kNHki%9KwqF!e_Qw2sz8YwV&i7NZ(VGmW7p{#ia%k|ef-n9W@R)tA=M~;EfNlL%Y z^;cZP<@n#72>SRI`to$>^hXv_){BXu_2^huD#hO9xw;2?#- zngCi7#_`-U&v5$qafW$L?i<`>1KEuD{2ia>z{NW;qf=xenbNvIinA6WBt@ps-ZQP& zFipkQ-U5I8x!d^o&F|-Dzj};^pFYUZ6DzEZMojB~md&)FKUc8OU*NKRyZFF0@8jA_ z_fonEeta6yOekE1BG~O&>)W74a>2F@)=b&EXFJ=MGFF$zNIf7E8Yd+ujvmDe!A1M` zMYX1&ACh_liG$%>Kyn6%JzxWA&lnSok%`j?$2raB(7#Y#w0#TjiCP%GWv_Z`(6-`+ zD=y`trFmo%wMb>KV02ytD}c!6_ECSryGTb*-`hYCYrMx8Lt`D2x?ycRWi;I&EBlm1 zL0x&O$rLv&q2HsJn@8pOCLG4QDNwvC14P?Yi3YrduTbMt?CjOt`Oyz>+s#+AFsE^D ziWY`Usesv^*(HM76j4|s3Yo@4KQK8H@y12RZxsL{YKUQxZF2?68=P^`v~_ER7xzQq0;|tjR2C%A5CV;#Xh%esp5x#K8t>kVJbR&esL>8n=8W`S(z=0=sA&W5U6<)2fr{CxI@3@IC-gYC$ zPOfnB%rc`%4c^o1_n4pSu{7+nFeu1`!E78yHWQ?+(cX}0fiPjt<(x?#-OeQ z##HQFT;NNe{uuw}Z+^g}9;5O(GNCZWa_Z;_`okgHckBQ!0xdnVV$%i@`Z*!{64tef z+oVZD9Ao;nlytF26dqjbT?p2(nQ85ehpDqD3^Y@2zv%{Uzwui9#u~W_q?_ct?xU_{ zFguFlYuolmH%%%WK9p)6HYOD>9y`Wk&mQE_=bq=(*%h+h0=n!|R}-qbqOy=L%%SsQ zrgVDuj7;Bk0iwP61a;?9)QF9j*gFq@^gEyA#w+(yjn6V(9^namcM<(CjWH1_+_B;k zV8$m!s&?d=lEy^*5*RO$S&(<9ojy3c@ta1RKB?Hs$X8Gb(ej+aVsc4EInCo;*cGLe zSR3W@goqOi0xM98s7v9LXTHmM?c&*3AUP2 zZ>*!HBb)Ov@y}`vMmyI3`G6g^%9C^Zq8A<{rbP{Y2JE!z4FE6vU zyn+$}TQ``xX5X$|{LXLv7W0FQW_20qESc0aLPIW4N~5w2tfi?Ycv~Z!p{XlEA5EgH zz*vKH4Q@KcjF<6F;+5>q&s(H1VK?;_r9yx;)?jQlF9oHw|fHny;goR*L6eI*rgh8kg!-AKjc%h9lx`<%0iX#5u?S zoCwUy^uR|4C?CrQPvRDY$3UxY?3UocQp%v3WKB?a780Q}XIIxa_~J`kw*6|H$Wb(? zqM-2(UGzA1dWEMCKFi$P9GC6i!7h_61PUBp*ua)_4li{m z%cKc97D|SZq)>QrtQ0t<5xS4eis0+%Jp}^n6irda_uO$ zT(*Tj`O2r+I)wGJCotZl`t!VaVuP=K^CvuW_zXtPVN@Qa;ASr5IoH%B@e@BorSD`Z z!-5q8A5B{}R%=w&!zMQ-8+#2&I7QBY$r3Vm(_#+ex)vnmZJ1t2fzUF-fn89VPP`7G zf`pn3j^gu+3uOHP)8%FIA}7lWRz@rQ$G`qNb}S6|;AMNTqh-d#a*h+lVni%2&K zmj*kFsq}GFm$mjAY9Ne)aSTzINCB96IZ;{Y!DO#G;v1lHCku zodwpP19P4Erxpw6L6`8v_s+GiFQb+OtunWW zBUBT$4x^PEL87fThN9o+z~z_m^uv!bu|bU}>(BAR>9hRXKl>{G`+xoiTz}bqjvYJ7 z*S`5Je*CisSZ^F6??dA`dhjGafBGf14Ikjrz1#W7jo0wOtM;>fZU@Rvn2uH`ya@aW z-(fS94ATK86jrKWf}{%!1!pIgXAT|X7mq&0lh3`#>Gc}p0;2R6&z|6ePafiq+djy? z-8)b^!>ArVy7y83{2%UOc>;32k5hTjq{uY8_UvLfKObxZV^2VN7161YlqF(G7g3HD z8*m6BnjS?R3oQex#rYsZ=+cxbZUickm<$M^qtbc+hv{;zmDF!yvsk6lGUDzNPQw$v z7VTO9lSE{dM7Rc=V}fI4dBjUUx`!v8e~vGI^5a~+e=pT&g*}V&{O|toD=d8dyL|r_ z4kXMSp6SX8ASmbN@LIj0Ao1!y-(f>|jj72y8X#tS4oX2l~2}d70!b49T=JGvDeBze( zbL;!BU~6xQ@ni+nj45Kj###qb(RfL1fqVe{B@Ulj<$*^Z=7C3_<%Q$RtXB?M^l?QW zp+kMXY%0Ea-{U;+ zsOup+B<;E9A(5>@SU86`LN^U%F`%w1etqx=XIH+#AARYw-1xrB7_Y9-?+^GFU->fq zexI-ZI)*hsMq@`ggIbF!dUe%*I0K(r_2 zJ=!}mJ0f3yiI3lSF~9f4+ZpNxGhN3zQ2j;jd+bI2=DQDYrWwFsJ5!lOMc~ccjJM_p zDodN8*~tVWsWy8e!80yO(N-=#ioT|%RU5#fyAn}j(R8nPH?3vwN2ioeEWG@?yZa9`W#zbq>Dt zV}AYkbNrJpeT+-CEn>$G<12)eC>hWV#(P}eW7TTz|M|n*b>G80du)YTmiXRQTwh~^ z#yJ^O#=S>p1yV>&UXe ztFX>TWop;vF1$l-uuejZc| z5^|;#m8p>({#~?^b6pQSOvr511@Q>uu+tHfYKn5uU)aXcjS+wP58qPNYT*M!K`8MWr$Hw|HQfl(yA`d=!gungn1DvWu zaIg_7eW8FQUWnLu5^ar(5760G`$CFtP}-Rl1R)sjVt@%^CteCf#61X^glA%%n`YnE z<77znQy1F{OKlyxD7fnV@5g!1k>?NLr65;1s=q+p)WP>b3cOI%me8=wbF8&teCid2 z)D(rH$Ola85%)fJn7O$J_!qx>Cp}f*swqXp9U1FDmxyA4`|p38zxeh~S#}v}%O1Q} z)I!k+NnIP9mw_wc8WfTuA0l;0)zlnYtsv_o^8$~iat^OF+jsBc;!F0Ua?P}^<49Aq z0o5h8iYGp$q9scCm}!v!RdF&E{!bL-Y1PC%!i8(@gzJb{5lj~qEb&T`4F;6kw&JA< z&E&KeW&+EsVPs3zU?%-+pHC0gp_(3UJi)B3Gp@!IJ;kw&RsQ z((#W!`w5g6{N49|Oyvyob6XhMhUv;G$Z&olvVLFbVV->!yz|(qqF&#?k0(&o2oJfG z7;4;P%49Ug%nvB%=OGg0&YR77A&<=nNlVs@(4&{Q;{$v7gD>93K-V<&2(2^pV2PhU z`T~Fc^`COuY{B=p(|FYi1aSd^18I;DXYk4AAylw&L@H&wc)jn8b>e6|a6p7;MW|9= zv=g?D9r@%~Ekx_J2mvRdS}QzHZ9RF}+5T*o?T}3^hu`Gl*ZKzd-I68=E)aNa(r#VfrSCiOmU*YdyDk~Cv(;t zc=X_5)?9|1+s0IutT&#@1#gVO(iR4Da};IC`uZAYPMl!eG-N^}^9+$k;1Do12A}2Z zxcDOW?B9P&XoS?cd3K_2geK8J9@p3ysx z(80gcM=8N%2ISIYR|rT7;qeeNU%hOloxAo#_k~ws>LIfnQTA}74W?@=luC1YV}<|t zweL_(Yd&%PwU}y5K8D}^)a_)M;;Y~P5!11x?9G9N>dYCiA(Ln~k8`r-wp?%?KdmvV zt1#N2aF$F8oM})#tSwZ=w3=d;4P#>|=jTyH@rps>Z4VGBIA{XSp%@?I6Cc>i?|<<& z7IMRAV-=)E^%uDRkr()jZ~mOq^#XRV6(d#q#9hd)ZhB=AAMzw)9xh-~xlW5KO~ey0 zC-g*QWMg*nIx$^VE#7L=XK)CFmr;s8bk)*sDUdWaG%c@u|Me7QpQoODjLCRRDK*YR zV>~J^x%}!Yx%{fDP-TvFj;*GlnKY~~pXS)1L!5l+5EJK6x@6=fYn8`mJ!&UVTBB5k zaTX^sCRVcENNQ1FWzGbF)tcUL$kNX3EG=zED1}y<`FaOB?{WOdi&VytqoY7eLt`DX z>~YcleJt(TiHQCtLV#2%Wb@WWrKj%xhKHb2^GX&22)RjnGF(R!uVe6)NWT*xC!x0! zoW5Ph*Kw|s%8yX)gt$DnE46uLoW~kUflf$NuZPS6#C)=RmZBJPYP!bXeD6nSo$=vo z-^XM+Cbyp7{?zSgE&0oD-33b0D~DL)s8^N&&~pPsmcKlMARs^8bc$JBBd?}tUqd}c z)HRqY_--JP<&ZKfF-SI+m$8juZu@pb{_;%X?av_Mn`X_JVsw;G-E=X3@WtCG{g`Sr zMu-A6*upO!JItSd?Wde_3;6k+7_UfR{R=+w5nZ25#Evyds*H}-9g+Fb(W_f@9VfuG zn@lUmClZC&wZ5fdwnLtXXS5$GBdqr8Vaem7JyY}v5xMcw+ z6?L@w6VW|9KoOH!bEJBoEAbaikdiM&%V~)m3z?|b2{;w~Mn$CL3+oVBEMp?IJiVWH zc19C{mj!}uvpx?Ul1;zYp*hJka(;nK3hFb0o>rU~ukxo~`wkBH=#AIl#v|NhgU^5b zBM7bdt8f2+^`>SpzetT@y0Q|uBXdKLdb82Eci71Uw=p8Cr>JTJHQiwM!hjEc@B>_N z*#QuO$DetQdmnm)GaF;F!5jlwP)#ewtE=>v7GDO^dRqg8_rXa#wgV%IS|Ou2MW)&no(}@9d{%B9l0CQ$3PJstyn$KGRIBTlx(?0M8FMCK8+=t!V(EE{8?STo`wsA5e&x5h?BX3{A_T-Q z-g+|+-})i`!{7a99y<6U?;7zm1{E1KtNywGe|&vPf%*t#@=>6lyzlz?ev$gJVusSQ?ZuvrO0wY7R< zEsae+DP0leyr2P!xp^+`4+9)pE3EKLUDKi(#SG|4#x6RoS4bQnw8X10X7s|hc7~n+ zdq+t`EIWmiQGX)@Ya$NC2@yFIE?%dIQHd$JkTP8FriSShndNah6~>odtEfvlyFOx@ zxut%;cA#jVgG{04=21#BJ#`9QSWZnx{KdDvkMo|7-*f{_Jw|M-@#Rl^l=-|M*A0#}zxb(yT9In@KQ=RwW;L z-^FbIul|_-`@j14Jp9rr^k9ih2&%PpbWtL+PB-I%2Z(l*DO@Nk#>cqx1N-^P=RQc! zuVboF5OeJ<@{0=_oRHyNPWJ?%g3Q&#;$MkIg9w%3seV_Of19b z%bJnYx)kYzE zJ!(XN|BB!+9W1&N6t#U~uo00ebOzpI(^N-vBf`L=pw-f7`weltnWoB{)BRaaO$xe+ z+Y*?WAdnFa)#6Z6fbq0!T$dWhB5*5k20I#2iO?kSGJG~E6yyaW%Ytqr;d4u+QJDs% zHYt~%caTsL`aRUvEmUU$ef{)ggTMUtUDQ?0r#}2aR5L|PMttgn*VEIAf4J+%96oaj zbcWU?)GBNyN(Re4AyCd>ChP3oGUQKw`}176YYWrU$0?(ltjA+*#pKixF4=w&|Ma)M zz@h)~&seF)D50q)6}Flpvtj$JT+jfKQgVr>uoH^uaXxv&#r)pqKSW=*V|4bRBKfF)}6~M-N08+i;C26?;T#K&WU+O*MVppOH!lqBJ65u zh?#ULyol+lXr=LjwoBYGBVjCTa4tL~u@@1r5;l(NqGDC#j{HvCTKCLFcuVAV}(FueU_(F&Tg!e#U0pM8x)fK1f<5P9Ic9QRpRdw&2|#SI?&St zcR)qc9b;|$Y$@O+mQ*LYqO^(+SL{i|QoyE(M%$DWZKl!&g(wlYDFaESzEXw&k~}M; z<)LuhC^fc~0~ktxYAWipqmTtgO1F?1UTDZd7Hho^K_S!R{W-F^0jliHr24NXNGS68 z1(Z-!%V#Nd#_D*Lzy9tIG1l|>kA4_iS5zy@eB!3-*|l{W-~7?t96Ei5Y&egT8I6sc z7iZ|}j0})<&K)=3$omiM#f?_UT#Zg6c_G6z1CLod!-ucGhU+ff$K6jnkIsi=)=`fq zWc^;8=3KxMqwO$zPj1KP(J?-G<6i#Y^B*DeYt)kwDl3q^E!_R!Q~cGp9$;N<$Mu(} zT`*;RD@U1a-(ZI1lak|vOK4+-^Q}}|=qZE;Ld5As;9}4cujI@`Li)g`@G*PvNtZ(S zNFR%h>pcy@m%?h0dDa;#$1geyIp1DR5$))$x2S|pbkU#EcUeII zJ%+x462a&>sVoKVilVUwr9o5fghd>So^mIB$MBjdg^S6#WJ6dGHzj(|3Nside$*m#BT#28TBr7`lia zT&!#}!Pz?AL5H(7LMWun;c`osxpW2x#YmVn`UCG{r!j30thcxONh2E#duUKz3i7i!lvq0;CwO_E|K zDb>@6phIb_ZDHI}fV76iGGnPfz*iN{H`u1ZT8r_9X>BmpVTFP$M-(N_dnQ%Q*wmP6 zk#b=HRg^7&?$xr1P1%N0h=D}wEliz2dBeuWGT*xQ0Y;7GcklcJJLb1iSy)Bs=FKu*82;3aBI(0DM7#~6o3@zd|!&7m`g$a-0O z!yO&bssnsEzz}+qor_btz=PG6mvv&4my+6&SKaod_AP z6*16R8?pnUh^&nzqP0P>JMk)<_4sPa)mLB3?|$YJi0K$%s@Nm3)OAB;8phL_qo-DQ z^5AnkbNDbLvrcbriDu#$R~4?V>22MH>Xm`(@EYs4!+FFHoaGc-w@|47^B2<*-~Gj} zdG4j7eEPNzv1ivVgn*Zh9p%i%8e|1oULb&6Nap$_eJSXBOLo~_b}tMtl|d&RPl5@Q zHHJ(pw3T>kDT|C;E5>xT0bOQ&fij5Hn9PL7Pu98irhV+$xj;QRfm8yo``q{N(|q-N z53tM@>|iIA&)%|8rjLy>=Ne2sWpPW+2S0ctH{5VJa|=0{mPi#-;3f&uDqQ10;Wnx274Cwum`BN2^oiG-_GuOM^@vDTt>=dIzRaBZ+AQb?j*eIkLy zEJsQKHYjF!Z(~n23YAC+PPW1*Edn$yK4VPaWGEGwsLrE{f^8S=MzEW@s@wMp+KWx- z7uZ-X1;9Wk7qmv`@Sry~ARt|&Op7*i>F%9#ppk8#0y(2j?NbKeTiJN;HCkgCJ9e(fKkY>2n!DK_aRVNvT^6N~{aiL4b9iN2%Nf3Or;P+57^gC@?D}BG1{F zR*aeoWeNTw-l3!p4=oLY7Gxg~0$x)k{hmKL+YL5dI@Om&*u zZ@ZRHeDcHe2O7%=r9Hi}hg4;lIC$U5;>RJscZy!N5Xuk}wFV+&+KaBYLaU(N?MkAB zV20h_M|I}Zxso!B2&M0wzJa!H5uS{YMTo>lh)P$!oAQO%@uq#x)PM^Zq~IK7nx-W{ zB>+-LH3Qnv+nCb(V9=X}D0q)jp;79i36BW^3N3d*&@n2dkXRQ=8zp6E%p!vBW9p6g zG?lPXn=@uygzN9)Yq`{qXdi6RYDNk=aUY)qc*tT|;lL|o@&}=th{RD-xK=*wo>B&0 zjdx)bN?!2N$uoTId*5fV$S9>?SQPZKjH1jL^m;7K4Hy=EG9{QSpJmU&kpK3Nf1khp z=J&Y!!H3Cui^yJ|(aKr8^~~*3$bK(2F3*2mr_Zg561k*lMmcsg!cE3VW7bj)Li4s&94gPp|yXB&*QNNZ4;rm-&cfwF=|p>~nEGA|aEi@4j(_sk(XX1 z^DZ!qr9=aHCK==?~M z8>de(+_?i;_Rebrx_K?ftRmCo3k&#OkLl_vuBvEEkRWu8!F!9bp?{Uh9D_mRJ)Su9 z5}-$!W1H8^jOG7`kvMo4FDY@KU0 zDQv5eXC6H3bIcYU8W*n(#YwXYv7CLHs`rtj<_9&b~c!6!{ z-61$ir8~XOq=FrsI6!5T3MjyY$eq+A?}I){=9nfLHn zi4|e`QkyzpD2(NFHQ~^iRUUi(MSlL+<9z0$AK|tS+=Q71r#YjeK!?R&WU z?qB0wpZSGLV!bcV!9|3H=}d*4oT6S^M(2`7N>D1M!*Qk_8-va`lnm{9o#|-Io3tZB zq?gfjB`^@v`%1iZ!Ig>bIrhl5UiMBz5!CQ?n+FJoOer|F^%S)&rospF+%yqMvTHUx zEujnAx<5OEd4C6RB}Ils%;By*wUh>{I4vq|&S z7$7B)aR@@{mdus<7)q)ziEuU&w1U_S&OxSe78Hsi3q-QOgiV;(aFLTPPrBbiD2#O| z)x*iCg+YVJR6I|QbcU=isG14Som}BBzW!a#EU)qTkAD=|G-L?=@C%=1JgxZALyuA{ zY{yB>WMvhV7Zh6-TR`0T9-FhtKvS2pL=}UT|HnLi2QI2OoMK zoh?$7^Pw!D8x5Wto|M!GxrUcfUC4#(C%}7EXbj5m;AtI73?(I5mPgEkr_dSWYQlJ9 z6v0HHUUxQ-m3-_LJ8vTkTt$T}-|edHuH(DZGi50vBK}->nq5Q|m&j0QCAg4CFuA2;XXuJKN02pB^{+Z?T~5ATg=U) zhl8LbUQfYV>gg2YJie)@ydl#AiiJKK)rh~l>xZmQC;YSD`T}-3q2KHC&wl4ioLyVz z!DnBj+`0$v4day+bY7z8%9wbG*R>yRDo;AdN`q{?tckniZ^>qVs~~^@{)W_dnpVM-Ef;cc68zb-X%v z>UGXbg3_&|9SH~}!o)zx*yWEd&(4Lkd2w+e?wH}T3kRo9oaW;F`%py|wI<_a4~vr` zskw*x(Z`^bWY=Q(CB14r9Y`P{5ho5R?7+cE$^~u9^g*2OJSKdrh}o75t!Ba|s&lOK zB{L&e)!{PI`Na+cGH?Q%2t=7_^Kq0MS`i`! zBFI(7bUbFfz8-x)1=d*R7Z#YGA5xnd>pe=whU*L}SEi=3ci|j_L=Og(OIrfc)EIo- zP&kXLEv~6(CS#h7b*yd3^C8N^_wRj>EyH=f{K=0|O(!hQ&+`YLzmwB{`L`S&tx*mZ zXr>jDjWvd)j>=81zZrjSHj%I(q})hJRIi|?7pbJ6UR%SNhHS7vfosnUr4fd2ekP^^m0$=R&_!jiS(LJ=w~BWHtlSd#%%!B#}d(27L0ROSIenVLFC60XtFAx@LESX*wF!&8EwfUE<0BQ>Ba<_A63bN)YDmo0 zg}@sTUA-eQE1-mfxp z$XG6xC21jBW8+v@;EiiXqEZRGjee&h5mLfvP|47%N-@jCz@SC+VMTD}fvO$>V4E#*gpeqTM_B*bUb)USHw*D-ZB{cizFj|LV8cn2zc9=9zAcsCs>hg^mRE zMvhkvDncOhf^umIQx;TfYna*~O@-_YV(l2PDi(puGV{b&myYPzwCW3+?bOH zTA}S}MY%SI^#<`KGlv#cCj}q4;c^b_-$FGx9l%UJ=kA}}%VSR*rRePhnZK@1vX8s8 z5DK&k1SORq!XmguJKutSy>Qs3;eD51#$}i6=c#93pf_9y%|{2%J@X9U*|Yx=%6=KO z4QxbGsK5(oQ+OY=BnXVc^w0&Ec2p&fJA2T&4B15(BRYgI;lYceM?~1++h9azPbVT~ z!u!zOM~IL)#qp_cxBWz3QV1JT@-&i+p!MkIA={B`z)pp&TWKAFkObu^j~+)yBWC9e!+yc_*IyR}U_y{`#OaR=!NFLI zwM}5*M$_dWR-;;-3fVE>!l67m)2N~(8w{zI3!1ZM$a|h~waVAN_XGCt*v`IPTWQu- z_{1$A;GxH#=AOr&NA!jW)6k432r7@Z_Uv@51A&Ln?v_{*s2cWZXDjm3|I;> zA(2@|Ru-UCyGgy$vvEPmS|K<@zz!V&C&{*@-RSkkiFP#REJdccWZy+NGet-!vK~)8 z{XF;G`xsfafRL}OwfCxr5a^;rXE_QRK|fK5R48gkCwdZhMeiE6ZCT*6pZPRTKK*Yo zrXnwTcwjoIdFrtzID2-Pty{OEGL4eqf${`=LaO^jq>oD>+rS}Za41hrMv=y&QsHb2 zWTerZ2OkiW&Wi|Jjx=)b0zV=o93UeVJAyep$$OGk{>?kvmI27vSdFC?iF}px8D_z5 zK9!gtrH`46?I_zm@U6YDaq&jjcv66h4OZvcS`=q2rmk6CIm`O;G6E=z5^D^T^);^j zzz4Ww|2~>(itqunjI3FAxO-4MZ|>=mqWbQd4|x6ewNc{MSa?V zG`x876!-r6*X-MUC(cyl-thU4f0RdGIKrB#$a@9T$pklTkb~DZ(Kz2@7P>8@MC2Jd z%gAOvlaFgVRBPRX_iE>If%PCnP&|%{GyNM;!mUNly^dGxkeVKArp)yf^YbO%H3%%d zvgE0!4zjvlQS^7bA&iBPpb8xvuxg8xg6U|)WITx=o5;A-q5q(iq-tu6gHPV^F`jzn z8NT_QyTA*i&gd5fm9d;WdW@4tkD--9Db;=sRHS%IHPa#K1ZPA52DETxAJf4QxMV9* z)8Y=23wcD&`6$d1+@xX%hm!M*=#f+T6M1Z<@-E*avcDcZJpC+ zPIJZO`%y|Gyuq680YP}0dP=W1#~=UkAF;f;&d=`q1$j9@=Q%~D$c4s6CZRXJb%63w z%%w$CCh$^HZT2z(mpsOch%6K|vO#zaK1?2xTZ0sdo6$t=1|Oi@aqD)@g<3ly9i43% zl4*>asN>yY>u?(3e+9x+LO$A7Gcp9Aq&$tVAy7(*b0M<{fgucL1Jpc}Dc-xzS}$fy z3GX3gm=tND!*%hYtecG1*}h{NpZ(0I*|vR&YGXa%9F&gdn?}WDu+$+*V5$lg^7 zmE{QCX*{0$z?=(ULr8pk28cQH~ER4oMapyWznPX}nJ6K&_mdLdoE= z-4eGF6Vnr~1<_6*3d>l7k_zuloXU8rdV)0#rPADZ(+zy&L$|Pf%OcfujM6gdPtXP% z7e}DFENB|b^71k=x}JrsO3h#>Ai!7Qr z$NSJYto2Css&Dg_9Gf5hSNdG%7oje2fQS<1v8fgsMlre2qD@fW@F9Y!sz``4k(hTx zI`11d}=!lI>X+8fBwgR$hFs8$xnWA zFE1Q=k(INnn5GVR2tqa`BNrv{HOveeF=-}}h*>($6cWT+3cz6FRg}>?B2YTw2s64; zNt4K2K^7k0nF2{AA-agmr0+0;SAevh0{KiNwkKiSOH}73sX`HiXJr(2aU#J_Z40nU zP+k=$DCsRE31%GyW}S=3(2zXlTCy-V&-QIwx$4TRxa^XBWVyynrby?|xsI7gXeKKm zgC+!$Ock6sdzK@|j*%64;4?^1R_HgB@~fhUD0;Y2M&n_1Jfe1CIwO^gzA7CJNr+>pEnXDy4Oe=hSA|U39ht4Bya47C>eQcB_&N=VWtzL5EOZi_omGZya%rnf;9Aeo_XdOR#w*;%rDaT zpvax|%eG`ZD$>zi?9b4Stl#Ve@9jJ;Xb(a}CZG3B z?7Bl5RtY&H82d)ARfNtaPYHtMZP-18dc!3~c!(55uaDUnVXY(U^?2!}W88c1FZjLR z{UXzijZi4aup#%IpU$oh0hFQ_IXtnqAHTwrj`6-=vXJGL{%MIQ}XHK{Pxi%{y; zQ7WY4fnSjvre-n29a&9A$4ZoiiIm;_6JoM17QF$Je!mLI0hv>2wjB{S^ItkG;25uEJEKnhS}k z*9lpfXuXr(E5`-RAcRO#VG0uweJ#0&%^;iCf3(oJ_+3b88|$eJ(dX{I}rtJQyQO250Q*@T3_mvz4?|aA!fB3XP#y+LM0-7RAZ8| z?YlUVM`=j;TS~s8a}kUD>olGeF+KFHAx;1(IqOYo5cLQ18*!vB#;R{Cij8IN<|cC_d;3q!c_eBVJiYMr(W@5kI$$jax55lcDtm ziCImn^N>VwNS>Gi>4L77kIY~1QqT`ZLJ4y2A{b1^Q7lobQz6?4kc@0BV@=%ACFO9Q zM;?EIU;Oe{=)4T5-bnE3EzAeT=o`T$L`_SVC&d=1eNUDj!G!i8@JAl8Dw^3P6Z*Blug?e8KbXoHKAO3{@ z@!$UkEP`?{z<6*ds>Xz`rNl}ZoTB57VOGygRMUGMmrGmPWmNF(hVP{lIh21d) z4Z2cHDB0>kh6U}~sZc6WLb=e1;W`^>&OVv@gu4>75fV|M)4>d8x(1vLBU4<&TBK6N z20TS1otdQ5neMlUej%~KkR;VM8?qx8aS&k@%nay5Q(LMq%2jb=@!o>;;d)3~|am8@eg8A|}1L zIi_OXL1WrhFhU;GI>wr|BX6;(BD>t+v8qD@3?1x@2Ejq^yY z$@7BgbQ1o5OxHz(4O^csR#In9jlxLdjse$3|BJ-*Oc;hPRZncl5F|_>j>N{GjeZfT zmA(^E3A@#^@HV(Xiy(`K6jkKXR%0_#C|EUtAu5(tE~v2pdrVAky+LwYPl z5d1JcEc{-XlVuj~YM^{WFWss7fXp)Hw=UrtL$kh)7NGM2?;PL#{*O8M++lwEOJCqa zANn9$7Uu)&Fmev84H9e4IWiA2t7+;QotHF4jVekSQ-^xp#s;NzL60G#5S_D;H7VNK z-zTkynR0Cw%;QNmb+Mkup=bOnf;OaxDowHUiJ&q`0zbI`(}*TN$sJ4TBFfg;fNO}l z9xer|md%;~6rJgDl6=dOqBJrfoLz$cZq{n>K{PUqn`e!1qp*xfSc#O<$34e5&#}{I zdHCT+`PHv~&7`uFgCWL+jYH%a{jH1SgMJ*3zd`78YNh$q$PGOt;k(8x_y8yOT`%Rm zlt)~^0O8xe{7lr8NL1V*-_V7cMYVYwbQxp-p-3&rGEmxW(tLd5=N}eiJ{&N=V>_!h zI4I*GD|@IcYT;Bqwd~6IR@Kd@LF`z_?KO2EjA~^U)GF1diB@k2;HFZ(bA#nUc zA3(O0UyE>wN#0sBL2~hfwGDaf5X_NIyS$b2W zDo%-_A7=={4(A2q$&}T#RbD!Lgr}c9$ja&}c~MXfdaXxkBL#yk^9;6bLFRcpkJn&I zzRDqF44y8$j`UO*{D*()i_f+EYwltL;+ zmS?EeY}O-vP*?U*{5HI47@O({A_<7a!0tqV@xHC;Es3+>!*%r{GEk#PN+Kq;P0IkP zN?IJWMmJ$3oIq0|!n;Eq9Z-hh*-47nNv)e1JRx zCl$#AC&9Ko2HMw8xQl@3lwmSv0~xC^mg#iL`gn}7hCI(H21A?=*RA#*k!K7R<`^z5 zA&atwlfRkXK)`Aw(VcTT?la2WDMNB1^j6+0dBg=AbH;iz>bA@1b09@9!wn8mGZOi) z`}NZ}&$X#s>y>#<*7C+b!i+(V^ptbM&_K&H)y4>0*R5uI-k+oF50FyO)HS0?&3ICW z-8nWByVvz-nPIJ)2@Y|`O#Oza%w(gpR3aFqMj~ydA);6MUT1@f*^wH<9!Z^j6x_G(4$_>K!##7 zGrEMOo=Gyc@iqjGpvtar*2aEI8Xb0QPpvgsSq5x`mlzy|(A3NZ19}T{lygH=7QjuL zISX%ekH*S#op9p zp=$=WB)15^_cHi@I^kMtL^%`8-Ydsj9w1tfh_@&aAj4V+ylDvSd5;nfEj$_%<&WM7 z7GAWw?<{f6*+dL`>mqe~ZZUoKTGOAOqZssPDnm1#FrBPpYeQW%NTIOCgi;}blw35R zwRMH9YqSJ|!0QrS4x)!<*vW(9w{KmstnJKc1%%g@BDisx3h!-XXcj1yQ8i(lCq&3* zNI@#f{>64+bk&Ajc$#Vh?+jW?D(g|YM5>G?$;QnD%LMMi`c_^)(*k1~Y`uYS6TGu1 zT_Cd(Df%?d;B03TD1^qH<5{6Z*m->z(F7729%rkhkq<(H2pNElhA%F8gJ^}&5~Va* zk&*XGbWxJ!S>T_Dl=-|>S`1OjFO|@-WJ#hpNu()|6{F)?8N0embR}!_xLSG8|QHI6JOrDv-JdN;7o^RrJtB-u6^%V{o;B zrbc-vvJBr;cr!tmDJt?6ytCAeLT4q$n6|s`qa7eJRlmtV*rB|kX(p7~F&Ju!Trrt8 zjHY9Z$?&qoM&Oi?A~fR?%sV-E;lb2kMl8)|yzkPz4ErT796rj6M^0k}I5HY35HbrI zmr4YSff<;#O*MTm#zbRqc1k82%2FX@#&}u>@>$kL$Q-mrWjah$GL6o2M6MA^w5k6r zY45ETY9SHnW}EVYR#vh7BZ6vIr=juQ2oi5;fM_M*y(P0F#P|#)6MD)beU0=6Ur*=> z%Rts_j8BsFN?finUcJ5>7d;L9%+4yJaZO8?z5qv?A+-?i=+lsA$UGm zEL5Sd;hQPBuldx+ZsC%>dl-*LJo~~??tkP-D%0Ra21o!++XG_r`jBt-%ZxQMtE@%*N%rzkD_^MCqf zK5_f?6k77!p`-k(fB(Po{7cL9=eI#Q#KjV@Ros?cD0hH#Hsm->MZJEC-HUxbf9LJ& z-Ln(t;E88mzcoc^0pWDcr(Z{a(> z-N#!NAW~$O&Z3(UV&fPGwrW0h#~r-?>WjgQ(XK(bnkz5e!ykP44(|E&VIDtr5+ejY zn-8eS*Krfm?%)%5p`B=K2J5_iM+8KuZFP+-puQPX_Rg$`MMUivp2n-RI0S1)gdxKr5wnSX0r@EMNHeN4Wa3{Wxnlc=QY}96HRw zlPk#n0!~S?VIMs=h<$*Lo|1dWyrEGIJBxyk-g-Uzb`7ZN2{IpY=%quP`u;=c#YLpZ zak05fi@8kbO98e*iV2I(a$xUP7W$U)`f0AcY8RjV)UEvK-`s`XG6%gSY#M8}Babxl zoPpqbk8H*WbCwT(=vw~iAAA-wT|*Up_UzliPwxFW!Dl%S*Lc%1e34` zTHuuG08BGGs`cRWf^y&yc7&ZwG4(o`vJ3|rnFS`PReIAbUu=36z(v6o;jz^^(hR_? zqHM|5#e%^=Q%i{vQ5;sx{{H5StQF5OlKJ^Q@4tFK%C6E>rx9|2da{g^4yjaB#Y&it z*=zrHrSFNHMk^ziWb4{#Q*0)8IwkL|2=SIZMxM}j(A5f8?H=&Ye)m&cvTMl3*<&cz z(C-cC_va|G9A8hUR*tcIAo(X>zMaqCayeyn3Tei#O|Y8jJxG~jZGm`iX~kY7o?3h?UbS$a&7(-F_$i2Ir^Dq&yFofDx965r9lc4jEnsY&Ome$`)@Db zA&S~WssE%9?xMtBOTBl=ABHzWUw%i9Hyt2axoz(d&4{gq<+nfk5q2$T#%GT*-`6Oi zcwtOMwt!Q zMrqZW-zEp}6g-*5NqCEr9;L$6QYnw}j$Cz4+eteD+UhC(5gnyD>w(r`=AYh2dh$F% zxJ9C;w*b|b{b2@TE^I}fIp+EW3L6Y{m53lCkC%xIC3xo=ew|k5Y?*}bGKzGLvl-lb z>3CCTJzF;_=gG{7YY%MW+DmrOjF-tID4Fx%Lyz-~@7&GzfA|33`~E$gKC?<$^yp<0 zJ6>U~FnsRz57Ji+!Zxkc*{gwA!o`=ZCX9^atxj?29gfr@py#~Mtv&>7wvcvt zVWZ=BCr|cDuLp@h7@fXcm|S?g7k(=dh%RF7cdcbY2WU~?}okDw`zN`wL%~&X{W%N z`YO?m{hdDL*LA!lVNKw{Oex%m4_tGAj0u{GVXw~%&mZRg2OeYE1)f?yizMXPW07yv{X5hgp?j2q}k)_zlOL(+U(W)<|dxws} znJDWfWu%f5#Z+Vgsyy$VK2f=}^k^Ngk@Yy^TD*j43liSBhc4MLw*Sf2?XWF%+>8Xh z^SmRy^EYw4$pF!{tE6uj=7Nj1Z$bGQVGVU%^XzjkvN5vc#THaC52DA5FP-4@*=73u z9=Vn%Q&Vcs`}XZZ*s4>Cd#y%g5;aMJwJ4G^(pv$we$z+0(@R8z9tWMmxJ&AQq{K&` zjQDFW|JVuI6o^q?29l7JfqNkn`7H&Tme4hmzQ5wpx}}DZWny>ApgG}QS3RHDn4;CI z2p%F5FEx(D$9U}qbVmf0*xF)EgUke3rlLr$eRu4{X`CL*oHpYXfr=lbB4n2lzSCLl z8#vxvfDp4^I0n6pqL5$=Ws%{$V{L7OkU3IkI1fq})Dy$%>RPu`dW%+``F;=S+)V9z z9*2*bjxF~iq;l^HagOLvC8Y#wgI&FGSTC?%Gnq6P8(_uO%o5DbJ@=GBB=@YY-jz{a z4GB$SA~Q1uhS$CZZPt$q>ee&wE)!J#JRe4t5Ro(WcW?`Toy$y!kadU*6P?SQah)&4 zJD(rMBT~(o%>q*{5qnVSp950;4T~thQ^#A@ISDXfkB}8 z^>E$<`FItSmm1TYUrIZBBn`kQwycx_-YpqK5>ssgGt(5P~V(D+TqTJbV2$?jnhLdEWR;8p||tQm(WDPHArMtL|1Rs4h zV|kwMJ@t_whAx?UMcsIW%&8lPvB`ufDRs5)etsu;0S*xjjYvgC6snaXjD1CJ6+Nv3u86c5L6$+N*h~ zsjT3oW2bRa&8)W9TAgWjF53qyrMU0-O=z2x5-%hMFkZ1yd)AG_^%j}R0Y^@)QhODJ zT74_m{<6b|g=k~n!6(OVY)ogn`8#bNqZ=!}@;P>Oaza>&)(tHPww!O64T301;6Cy+ zh3_UB=kZFjS)Z4h^{MB%!^_BG5s}Yf<$%g-tXI*F@ST6qX0NUDCagi(O;=K`<8iJ3 z=ewdjI?v-x0;Oi6q0Ctw)tp-1;L`0|f|QvteBg#_v5n=)r=P<*$G&}6amy_?P?j0% zqY>yHx;M}AhU2NjXA$`z@Ep$P0greApB9QpIt08cXtlF*e<3xLJ$`)uL!4SYL%-;; zKCXED`NL2Qa8ieHE$Wp&y8z=HLJ3^$qS-BES|NqRdPftDc4Z_zrN#K_S*>UZMs0s2 z?ux-}`}~4!Na8yGj#QTmDLT2i_WfS%bz%|1fg6Y{Oy!keY{ zzm4idVFT4IDLP(SnD4|+& zUVIQ^^+mfRQ5H{j;xGYM;SkcGgu}doMBXdERNQIv zd7eG7!N2^o@3C*sc8r7PUpj;9ZAayUKt8(wO;@c_}5vM+Ch)R%J;bm;7`ozqA zZ)Fd>*#NOgD?w;{xrKWleu3++*vm(6+)uqGm`pa1!ZTc$LrOtaPgxs{5jrF9&r>X1 z#IFyY<~u)q1f%EiLUkC3^Stq11`cQ11x2#2e@BmKQcIvCTcMGeg1n$1Sgn3hp0Fk+Ln)bamssEeE*Q!Pghjc`A(jW$Rh=&VtsWj9x&?;B$>LIgJM^9C=xy zbk=%n3LwLi#TmLRb42z^-b+4d{URayYRH(nYoyRjCpFgC=!rSoAo`7`Ix`-X(P|<| z=(~1AdVb*{NZ*CQROpYm2>Djt#TM-O@1NcM7f9WYU%mE6db^G!9PDD*C#1s5oP4;5 zHWglKL>{Q}$pENrtiBsQzTPk8;<2k8^yS;Rf4r=WR+BudwVi5FI`^PJ0kaZ*sGKJC9bhmEa0K z+_H!n4$xAfltkxQJii&t@+PQm9ND&#Zrx}0!eN~WnhDOMghwfjZ46o~s=A?WEK=qz znld!=eq&oB2>#;3LPv!mQu2{Zgs=1ar66-VkUwm-o6ZhG-**@O38I_$i`qtRK8%oW}$l z);5RUi5fM*Yo(IGc*ku9$-Hclq|ZGAJWD^B>WdC`?xWkaKxx%V;alsOPNvu>(i-4~ zi9Vm+XY+Y_;p5O$;Jn3=g|q0eKby2PU;RVN4zMO7j#3De1nDhu79INDc|O`>R(4Q$ zXsRjo+6uOwl4(sgnB%FJUgBT=PyY)$wk**b&hf&Llc>B$6%CEF?D{_MXz%289B;i~ zj1+h@u9(B*eaLGG;tNTZ7Cqgi_eXabh&#r8Cckc0*nwdjpipX^f?rPB4>cP`!GOkhk8_ z0I^9WSjdp_=W~c+;bod04j+tLW`5_qAh;p~hjO5}>Z+^Qv8%_g9(@KS4AvWDc9i~H zpUW=a%L@lj;$0SvZ{LYbqGS4X1$3e&&;H=ce)F8~f-AIeKG3|Qfp4B^goxe{9;&** z*dQ}6gbL2%uaW_D{-{Z)1zIPW@Dt}59*8&9cM9WpL~_M(F08coXmFTaJEp1`uf*Ws zgXZeAn&54XH&yuDxk9K6m4zNeQ`cm9PGtww0!_j@8xm*5L|5XPwz#bndA{08jGuY>y-z7`Yzj_h1J- z3u`SN87u<5M{6DRFzh=b;`3G<354gYWwO3OGa8|69ej*MLp5DT$%fugGZ^-8#6s(~v z3%t_^B~ilQyaXC9zhWqsei9h1{{#o<$$Zw+od##c>f`neio8p=#iWZ@Pz z)-tV4fSOAc=I;I+9K$R0$C62o@E#S7ZBm1>9W|d%fQWh(jZInzoy46|@2%@TdvqI} zz}1Fkv=L+#g}~MmCX-WKe)+|G`OBZ-y6fLZQ7WwUteoB8```a5cYXh7Ohzl@`8>!R zB?Q&RI>n$*)-U7q=-u0oxUkMzZ_$TNfyOz<)@^ef*tY|nd7Lri8RS}lS2V_j-P8n~ z1f6FbxMVl0t0SyAKakG5<(SQwTwOC-UPd+*r4snbgdEEaS6@Z01od=^(gi3<;{ZHb zX{5@KTD@8mB8*L=uc>p<#%4y(;Pvp61u6o$WOyCxP|l;ALkoda9`i1MN57tZ-`-npE!f5eFBmY$m{n#8On|X5OUMia7-}k0 zRLzG}J|$K8ntV<|RVt}e7(zUd37LVx5WtuigE0n=0b?86U}VX%TIzkf%e$Q=AKrVf zt{elB+%31veX3feQmOl%d;2}l`JeytJVlFZuRX-S{#Sp=RoCpN(^;i3DN<-&_OgS# z=JkKh>ki+*XaD1WXM3wl6owe*uz5z_>7wKK+>1mn688A=N(_EU(lLKJ@0w@5xli{sjxC&b*oL$`x%a&sH+MFCDi9b*5w<`J2(+LQX@s)`+A%vbfmD8wE8NS!l)>7($J>HzbAxPsjjDG9CMVli z<}HU`#V0@dK`KgMjHNa)#ZP|m1dsgcD7sQZ=l~%!A_&TC`B_?-L$5XXQ}l#R{J=%K zpL-Dg90%W2NI}5y)9bVoLl9QT^MbKj%v;`Y11eib+iuyRIaKPqG*C+PJL3?}p>X7R zo9kbFHP>B#1>Mdn#c$F=a2x zPFMi$5-uHrz@wxh3>4PeGWpeeaFtX&I|PT4poK&#k5-OA3mT1xK&vxR#2&>Uau8)= zvS@e6HnxbJAv7r}+2WSNui=v)x{a|=lXN?Tjd7lMW`Qq%dTSZVqFI*9f6j;J_m==63FRx)~Gi$uYKLMDCzOe zpaO|C1=bWpFS0j3k4uOk`b{QfZN0Q!a{~id;Y(Y1XKl~_6M|5ql|bv#VS7(aaylE6hNO5W8f`-u_$m;iq)N56w^jMqYoE=y!7N#WC(#_rBea_5ulUgOj z*`n+f>5=M?V$Yt$B}5P< zTaWPCm(`K&MYPS%;}u?9^xH#_eY0SYQjzDTyn47HO_HA6USFRBC(FOPo-IJHEFDG_ zln#bIfxY=zTsj0H1bJ4lvDw1=Qc0=bIAxqCEo|u|-4hLU4xCUF*0ZtMCeMmJ+r8#{ z+Lb=bN!M0LSC$CcTj=%{kx97iA06T2x4n->C`sBa8WR&7U0mVc|F=7M{P-#2<}^ly zq}F3Rs9KeYnOVYWt(SoO-66dcLtYsJ;oNgjqIk0a02mcXL_t({4(R()I6OY6&~{b+ z_cwpYub)|`(wIVcLEhb9elp+_x4(m%u57~g0?H=HOT}^p*eBlG#*|0 zs`iH6q*7?5OOc?$Bb7#oh?TVsx@kcWL}hBr6;eO_DfHo?JrotmFI+mS0~8h7)zddu*1`T3#k!Tcc`n zRN5gl9X|B#xAXB2ypLEqvZO;}YMRH6E%4dD`XWy*EFH&0Y= z3{JD(bvg-r?}W31Q90+p_HqyBN)mmAapMG{I?iHCa>v)c%hL-R)S5HkJ>9Jp4ozu3 zdHXHA@v2F5XAy0ZfrQ>(P1NDbrZgSIrI@ex=%~UYk3PxCg-ya}0`CKylSmmLq(Y#u zB0$GYw%R!lKk{n|Cy_eBdVy5xMH>S+AME`V4u!BvYSvcU9DDjS&PQk+lUm4~z{>!O zBsHGY3Y?6|jpo>~CAPN{WKUdVFXSQ4vMO{Qn2Mx%RGQ zJuOAy=}GpTpKM>hYgPzBPJnC7@vGB0cYO687S=PO`V>+sx~(;)tDaAN@D|?miWy{Q zk-+7H2XUAv2F}?1wLbUjBKv#TN!f=uxnjkkJSuRL3 z0o)ZM>VfkiVK~lM<1!xj(W5-{(_a!*n|Kl6rA9~bo{eaSvG-Tlf4~QxhnlCJI>BR) zJwuWyf_Mz6Bak81ho~S%M>RT$;E7+IfmnvtC4;Wm4-LHcRL(ui&@0s5*n& z+F&XaeEK7|a@#xKN@z1okx>~N=YdBb=d)k<5+^rX$gwF>rAdXt)+#jSFC(59$Mr>f z&vEZa)t@5mO)3u5yh`ahc|NV-{Y@9iCZ9s5WPC&Xcv*nQg$I zedry$<(fHkdlBurB_$0D?hgB0KHt|xEDw4CIt&S_RV)%mNEA=-&AWccm;d%V#I<>5 z_gzhWY(Hbo{Y=hW$=KLI?z`_1?)dAkk>-*xsA3W5I3|kXJ(_hm8@t*`xsw=-6G%x> zSe|_1IP2@1?Atdu7~lKe1FWnk#MMa(r%+LaxZXg9d*{&lUASZj zVgO&-!g=R8wXn|e(k5{fptV9sfhjCWmSKw_3H&`m4ukwhFXzx*T|;!+=sd-4Z!q78 z_|%7P6iTQI%B&BQvGb6Nh`4!xUoM>Fb6>ujPu~6xu0A+LcYTGlwZ{0^6d%9s7J_fz z&wamq7EzhS>KcW02;rD$R2gqJ2sAk3QGvoZOQ(~wzMYWgJ%ONIoN4ImKsX?*RB>bD zbT`+@Oimb&Ve&5D_&@h?-+d1;Hc`i0Xmt{{Hd}}wrc#@xumT@ML}N{&YN?R5SDwq? zg-eehhCW!W0=)AiT?3di|3N5}dhrBfo)5#ut}ZWVudd>gZK~c-v^SUt;L{)e0B?KU zb)>CLyb!3$IDh}$`}oqG-=ZS|M72)tC7B2a$H!^R%_75Spi1@osuj7Ww>F?&VM4|5mQQav%BjD(%e`CK^-x$*pfgt9!Zc z;iD8`lj+&Z*tc&VQ{y4RCE!x5F?b=cR={LTV#YZ7>?UiQSx=>FHwT}3S3{|Y#u_;9 z=(e}W@|+;36X^=uorI0nW*ONiMO2vr6_Q(z3}dRzaiUta7qRW_$f1u*k|2gZ*fSFA zXJhydyB9DReYb|R)xve!L`8v5TU2bupWOBV-hAT?r0sP)pleOOdiVGE>pQr(KSMlDn*-lv7 z-o)96Ypl?{CTR3ab zjS0SX_xJhI*T0R4>JZgPoWv+iWom}n^bAD7j?FFvbe*o(|ODi3O+CA-$vkoSuiR%r*xJs6ED6$M|4Axl$8XX2G zCD2h!SgDkqyu<2Vmk3_61hFUK9X9Dgo}-P$wA)O@0k^*E?L-*5c}i_+iXZ&!G5+T_ zzD246WVKFi6`9mjXJ)BRPeBlzhnVc|fM8J0bUlS@M`@kt`&xD7!e5Y(T|~Jx0=q$M*HOh1+AMM9e8d}HbeCX!ps&(#s);X7R6EVbz=WE7UeX**T>_ocJA>}gz<|5mCcN$<*@^&OO;9@glzLgJz( zYmD)gdmiSl`yR&XIzgpEs9>TQQqeiWw{WgSpmV~&B4vRV38L5{a$8(|Xo5rg8swdI zChDcTe18IUw|gY|@29W6)wf{izjpuMFI942L=YDbJw*oa4lPCLH02Cq^$MXXMP>`@ zdHUojGAEEhh0Hp_#yFLU2}BT{Wl6YG51i2kyxSeO%5XX9ni<@lt z2H*POBRuffQ*;Ul>oL`OgeSqfE>ao1&naw%$D%Z7S%7QNtSk1D2|Lsv@D=_IVJZDNra5ny$QuGWxI z*t2ij;fL*v`2`+d8XGxV`ka=T%7G=pu0RN^S2*3EtHwCK)a5q|%Out#0)=roU@i}a3AOSBd! zsYvsJZd%s#Egml@GZX-Va3x{rKcecsjxAkk|Bh=A2?OSg!4W^tRx ze!Ym%O>#d*HxJ28fVClo4M~k6vk`=oY$k%EC)V+@LGD7@i9yIw^?s)?B8Xi^8LE4n zb=W+|6$Q#k#6Tq0At*H+!==oHXqAurd zfp;G3qnC~B>C(zxNPsTi?W^2KI&{yl{*NVwW(`eQ+L=rnJ}Auv?o% z!V^jl)*!t_ScmEf%`Ti?qOcwvls=A12c<{FF2xW7T?I$4sv^)z0|xIM0^18QcYm{a z1X6~qZst6_(B;ZQ^Qt0SsUOHyw3ECGFd4iGwwJ65qDJDx$fj+BmVQ1h` z%A=)2cvsF2cu+zSDn%$Q#=93hi*YVEEhxFQU@C;U#b<4_D61;c zdvFC>7(~%U`wT5&%#b1P#eh^HNosj^q0Q;VcIjB9%BoIQSdz?REFfghn0Z7FyMYlw z>@fP94{Is9ZG6(DA_XKJd}ot)zTq|ei%)%&=>W{u5)w=3EJZivz|<_C`P19!W`=Lv z{}5Iw;^qW-H^Wpi!n%IJCwaf*8VHB%ZcuTny#7^}b6{oysDTbjX3ZMl?v-Uoz&8H7BC1Y5mF)5b96yQ*l~=?oI8l&={{=;(pCpi z7?iQNw8Q0dQ+)ikf6Q#;$yXPMiY_5J+Ga!;I?D@0*%lxB<9G3j19RA@}5h?J%6;wTxp`xp_#F2R~oA3zE4u0WZB!`Hr&tLEoO*O#bB zOROc{m|o{%L5QVjt#kFkIo@*P^#m@5yn`rmOgF(A(;LY?|4GGxOrP+Gbc@Tv4uAfk zBfRg(tEq_;o3ycc2F~G%43})hqqmV#3w96ssaN~8apb-b9 zV>u4899!fU6g`~TOb%7Fm{M&%{{A=frW-DY&I+cpL8K(jMuSRJB@RQdIqlU`OvRRe z_Q7{?_^MfCcMa*x;F@tUPw=xO@LysBQF|F9g4h*!q)KrJ6mJ(Cs7zxh1vO&rgILdKmrBpe3jqeFh`<@Hdxi8~P^xzTR(9i`(UjHfr z+a~F3QBj^srN)B~{(|q{`v7TCaPu2q$6Mca6JZe2O;hsD8m~NbfWy~a!Pg&JM1~El z=Yn$~BV0NdMG(6HPhb7GR6tfiNMRhO7MD?RjL-_CAGlZ9yue$?jln2^uGZM-CUo-x zf}kAt*1G(@3*3|v5GLWkRE^8`PvX-S3X4{Xlg}=4-+lkU;&O*hS8&hw9^}UlK8n&Y zaS-5(f`EiqU2}+v$Puo9{s_k?QTUR^h#+!m2vW1^5`#~WOJ*F)FIXs;Bw|?nvB;Z!lY%t#0W2Wj0j=}(RX0- z0;E<%wJJ_12ts77%EP}o#`k~rDDliZd0eOCC8_nKUXXf4CL^MWITp85?s@R15UvoVcVrN;-A>q8-y{q}q!Jish(b+0 z4sgbtQPmz@GhWgd5yXze56p1O2tt!PkFGRmTgB(U@=bpHs}ocYz8pWkkE}95TUN;` zO@i5jWR)rY>hJF2(W9r4wFwHTu{uBn!CCqV=Zi86Au+~dt?zAciS-_>6&6onOmAo^ zzO%|`NhHp|MtIpfxo-werQbN!WyX;s9Kev5?`2bOK$^vz)QvQdW+P0 zg1AazEFRB+gYyKDz?u|qGqS8jwGuNv-XzWQ(kD{|bd#K|Btz-A9O4?`C67@RV~5ed zW&kLy2rg))&kfX(=|e)jpTW!O_PV6AQW;ZL97s@va; z2tmyy|&+!(6kbF6hd_y6n(jIQFPDsvX28HkrW zMg*~2=$)`aNTRViNI|~cB1u!Em4s3wgSgCD$WmCw3Pm9lUP$y9YV^bRda zNNm{Tdp~`GS6;D?H{EcMbkosEIt13S|Ii$lzkD8JEoqifct=#9Bv*C5bMMdj^CMJt7eKF$-QN4z)#66q6 zomopM$fB@8jg?Ny9bf-3UJ7oyZl17~l6AHyoPo5QmeiFoYPB&^8*tYTAK}g)JVqMK z;B;-Ewmpg-UiuIRZ~fRP+qBC#)BNbc32c$$@*HbjX%pwcOM%uB8OA8B%gwjL3Q{FW zfc6HNY%mpcc<1Y{;-(v}=D_SEVOY+Pcm&%c7Mitv!HXgBmW}IdJgK-2bEoNB$G@$F=r;$eeQjK!&_bbp?i0& z+Ex9b*IvDLjGBrp3L+sQ002Odmy^-}0D$oSJKUv!%n4GM}q;0Rv5k9u6-Gf8n#+!qznvBEe_?Y@-#t-a zTxjQ!z62hBSiGfdc_~*7o-h}!2fsV*P$na zxM4S|QD(h=FDp#+|3;TRlYW!~T>lT@`mP(B{C}g0KSYc(L8Jc%)je^c0qOh?pVjEm z=l=^RPw+0;-sfofix}@5Wr>KGqSWJ?KQ#JB#sD4QrDK5f^ybCvb8F#WM^Mth#YNz} z!_BjC>*LM!S@*=8sPyqr$)Ok-Piug;ydX7Ylu#pM1u6 zc92C8rmNs%Fk0|K@IU|Hi`|voeVdOSwimJIpii;hhuz=qpQ?S2yAN)KuYWzzf*-w~ z6+VLluKfK93SXZ;ULSP=rQdh)*jbwa7$L=?ayTJRCTA!0ZEgolqhw+f&u3I>^l~@~ zhQ?w+5z$4I~CFYe>hhW$%(gOLL!Nw2mwv+N8vLbTZIdYF1 z2lVHY`-Z1r0}WRUnCR|z>40q4IP2DUc_E{E5<3VHzVo9hqB63$rV_{1H{c-hM5k~@;E1%zJCZw^+ z;q_ol`y5ui)pLb8hx*U^&-La|W1LjrKgeJxK?*rfkSZrtoQ)8(+*G0sri@OzE=dQK zUd1{!s0p%G2=IYHKLUtk4kw`c8T$Hl&{pobYih>FZ$`Qlwi z_+4yqzL*3ePp}C^R^*#^2f`Y4Sh&X6E`)e+n7j&#uVQ1P1Ai_N${bqodl%gHKL&wr zf_Ld$W}GS;ea+xjexVA~=C77a!YD!eft5dD&_RUgayWabZ3a0CUc>2O0#JPoSTnS% zABKR0T^xf|L~?}15f!@!k-9LH;xJC>D`Ys1>5~X&^yZPqBZm9c1~FJ4*y}qwvsndOo@+4hPJ#JaIRN#^#xi(F4ge(Q zLB?p~jGu&RpgcetH5NmeCWvtZg!#wolaB6w^UuFLiM}AjPavxMuIe(^CVMxQPHtop>{c=pR%rA4`R;>&f8t?PLx={-yWAAG0Ih~dtMmI zT+RR1pb!~wj`V@i4a5dYUO&~U`P%YqO(PYg|IRoh6M9h~Y6F}05p~Xet`r?TISur$ zZ}_Xys?fH+rs&Zc+4x9Dwm-B-{(Rg!`e$zFv-#Cv+( zcYd{BjTv@YKNX%k2D#sE!Y4yF_QL7fA$XmN&?2_b34J7qZqBf#`=lN!Ot;bSwgpDj znd&?!ON38S$-;T^K-5F$z*>KW(oH^o>E!#HQdmkR|4nr7jhpX&{%IaQNv(U_z{G3$ z&C57Rd5Ria;$C8&_(*6*k#^n%@-{Bvzj{eSKg1s6irs{|&jh%MS+?T1G%jZ_sZ=)V z5NhG-hoVGXfNamzyQ5QST*QJNEa?+j^DyodE<#}Yi7alG=(Iv|z$-}off@4dh{+OH z1=yk?PY&C4JoVql8P%J_MJ8q_(}jd_<6)P_y>MJ+i=Qn`WJ)36HmPT`OjbFI&$#tn zm~GiPe-89!6xK<#8UE>z4b1)XA$4RY@?Z(=Mb>Gl!Y$V{rfQ=t4f2gNGeSJ3hib|1 zW=B_F3z9u(+iyUD@gKrZq?Pj!3;4jt)jG&2L%B%cHo>5RsBfyXBUbq$aoVWY z4=|lQhYb>3b80#$5RP)U@EoSb-+O66NXce5PL-A{%~br<%_s%ZDbv;+a|;DE6R(J4 zXZWTOR>GTjo%tPF`c1B|bjE?u(Ns{;0LyC6bfCfq>CQem zoSAkLTnB!8bck8*I-8aAp*0)Kp=e0}SsX#i{i;$#A2im~B2Yb!q;B$3FVS+QSqqGs zTm|R*$EHY)8m9H8(e;>_cRqi-3W%G5MnyC3VDk7ySI=mWFW}vx(e>j{EgLm-bPR{) zK^ubaD9Ox9IvRsT!1liR!@{9w>?MS$kq4@owvI#~5lWU-k}xS5sN~L@&IW?$TB+FE z)o295-L2%Of4^akjKm`aawNM?p=*r>llDK82!w~^L(87`Ug5(RNF=Wc;+zu$4J<)k zh7{8zYOn&h6ygZPu8z_|O&O9J(-?%-h+%u!n*c7e9h*4*_R=dGnq|@m+lRVARf8!n zGD;$inNf1D7e+3*>efK0X`n$g`jqKOV)81X0uK5ECbD66lD_kInQA#`9R}T~zHeee z8~zRoMVAC2er8l42JMW{wf;0{v{6@h4bx>6m%ZkhrpD%YF7Mr#U%pXe5m1wyBef>) zIsqRiCJC~_M{b7v!Caj>!FX$J&iy)hR9^X*syU$l02P=i5alh9rHqJ0B1k8R< z{^oc4y*8;FmhYQn_k${qymsyFpjHz5^xHc(hngywyj{7B5gli1P|E91bNSABE{)_8 z)MU+VG7v~n`(7&1ZbN*JhapGtiq1LNjE7-T22B$QS*#u7-a?I6S~s7qK~Lf$7jhjF zGeINA0#-Mf!ix_AXeRVm``I?FwKbr)rk*erj7R7qQ!z$YpAjHVey<_p6v3hTg<~`H z{tbh6=^I=Ke=eiQhsm83=zybMyOW$L3K_9(k-)Fa$s-{LqvY*3%tw#0|1m5|Nrl?A z>!g>HrW2Wh1b7aqF5ySRA_O6B>#m8qo2FJTfNY`(OfiPFh=rLOd`o`#kN#t}#x|~T z+ty3SzukoOfx`MgEb_M|39ZIP59k~J8y26fEz!ttYo(`x=hvIhNi|0eN1vezHU4tI|*_#AF@mm1-XT?41)`Dg_ z)45ggu0z=$;?j!}wZ`32+dKT@S}64OZU$2j4dY{;01zS~rUKw-ui%Q${L&Qk58q2j zdL5UkR`zZmZ72S*_hcoDnUuldh_aUbK@}1m*Cl-b^_12T08fMmt=tl?2gl6JYGL&b zI3_ry`HA3V|40d;bjJw&AE;X0h?gI~;V`{3y?`d3ILK6T_cc z`2AN5qD(677Jq8-<}C>0?rZoVAxMq4F)46UmJv<%6s701IF*G91H2xzn1LN_Z6oq> zjOXbiH3VwSTdW9=Bz)aQ6|Pz7`%c9~4$n_eEl)u{r;>|HhrSVc1jM)3O)@dZ2ZqHr zk%Yswty4*+bi$6&XClv_Tb_w@^5+}0EDexAT5qgWV}?wfgK>4vAdE(AFfE~7Uh#F> zh{3eySGsXjEziVAinY4>MiatFIbJ}c-8~ZLPj)XatQ;@HpFiuj2h$PU+;%Yv%DU=Zv z!z|Gz)||ZMKp!1uqMSkzD_FVIZ>+^vYK*h@A$s(8l}a-Dg;u zGLNiR>jO^L&S_DCpk$#|fEt@f=CJkXVo-ljMEvw_ znoq;7haVYWiZLURn-nN@!dyoq9-X>E%6jBtX06{JPuh)KvGHh*39cRK<#$EIj0m$syxKKKH(y-E{rl#v>EAA4h z6cG?}NY;o8D2sHC#)!T_N+R?s8CA+rsZAcs@{3t+ovmr+3LYgX48RT<5?WiiAGIGR z#*a1WbE9QYdYZp1)xeaY$E4ncokT(g2IS~6ha9LRLXlps!2F|7sPqMV&#n|K1zH@E zT)B6I^TJ%Du$BHSLHPv5*Huo8tywQ~8#D>8&kX^(rRMTIDce@1F^+|*_pu?3d(<>% z7ipl6_9_?B{UaQ|t7+5i`J7%2F2FrNULos*v~&r7yEW_T+T67@{O`%a-nxOx{`5@J zm6g3QbWE~8FU_qWRmPFZUst|iLy>vPg zyo?rS_#&&0OmPu6BGiUvM9LrGcWVYprWplnYJyuY3pVhC*FQs?l6K`1Z*~0um9LmM z3)p)^tU?(@5sH8XegG36y&&yxSpAML@W#&e(0YW^O%Qs8!4|*wgHoTWWu0`qbVf&vLgT{wsKg|gTkE)wYnX6SE>+TCnjmRhOrfy7W|x`TKjXZq0) zWTp`b0-~YCPrKee`xNS*4JkaB)Es_u^L9wHsSaowW;)%87+ zpQb`J7A5!SH0n*Yz+5P$WLl}S<_Kyn(#qHqMgx>0F`d3%hUQcGH5t89h7ImfFk!#X z;+9)73{s}tUr3%5sU>ePgr(}Q<9_jC;Ew9FA514+4nrsorNm>VP{Wozol@oVEI}u3Y5e4KOodICWlbI!r~22M5%tduExsm0n;v=5(9l?l zi_~o0Gt0}pNM6^#q$6{xU^_gCZy+)J zp&Qubm2D(pkn#fgr*|qAdmW!mb#7k#e$gVw4OQiLs>zDJe_t0?BLUwtf|KgydAB96 z#1AYxp)t(5h0tz~f@jV@Uwa?E!0zL)_ zf*9L^^G+p6$x7L2qnhn2FMJtB)TU9x-KNOP{#_FTkd(q0`SJ1r+7Yo$cu?N-f^feN zh}09Aa2uXcs2T(4^@b!MYJZxi$}bpg@o4$|-uT-)c#CUv^^M1d|6p2o@lALA;OnzM zQ&OQXndBRWr|V%(n<g`x}j-X zN3;({c6>B?6VZAR`H3mvCRs!tqTQrjCH-Bvdau@xoR$}vDzspbQ&?U4he5U*hw&@5 z_~;oadEUF-ZwzP**})v)?+=<&VNk`*cZib7qX=LXn`5w;QEA$qB?(T$*uGs?^(Uf# za$dJEx~3MIOw-r}i^98MU-C9SnvV$PB&{yNRMaG<+7!S9izc$~ew(-W|ni z!<~kJ%Em?!MJKcCFLPMkte`H(sVV(&xZ@LBI5sU{4jbca?rBvOH%cFRRWzb*dwS%k zeV{hxAIH@!gDe5dh{XMmjAHiSsDwXQjMjlc)1#`S%w;fx;Qad$qMCp9DS<(iS`3r; zY@YZk|MSd@+FZeuks&q1Wg&oro+EXAR&AA`?>q;}M!^k+@LLPj0SdK^iB@&Azl36O z9jkQW9NvxMj*T}I+<>x`0ijOX2 z8%i5LgUIFDJDTXWR2tYmJxZB|KB8HllZ>gbHW+T7$LI|oX8rT;aZGZTX@GqKXy_am z6NBl~Tt-EUm{&?fMP2v=#Jnb+3ca-Q&I$z{j)KxGQsam|LFHziAdn9PS=`>7e z15|K3LC%m1=;7JP)J$}$>aOh+gS93bPRt|v>PzvVy)vR2B9t}0{>;@PZBA?lx}3fA zS=}+cR2XcSX9iX%l<;6al2&IC{N`>9`p-?>=J;woTKbSi?y5syu#5Do9}vBgdHXm|dw z(9(yLli~lQ2Eb4+vV(cJ5yiGvDYu#OxYUIABvlL|-d<9K~Hji?Pq32paLat{9 zqaMCFh$2`U$~sHt4uwZ8lfm)dxx++Dbixlveza9}AQnTByEuJwQOOf=XE4FQG9o5s z>&Ir@KW4cW8`m(eY9*7#iM*ybdOw2CEV^&dG6j!Bb&m5DbmPW5?n*H&r3I8+0Sw=5w{#f&D zLYTW5#>7>%koX)P%lS5N;SgcD+KB~goa!7zQ%#eBRmQBiJgmI9!;gFPeRny=EvnjO zZMr-{N{%_XF0AnJQO%|4u11Dh|FeGjzNX#nxqJ;H{>(#6F1fl8rsMNt)JQuukX2rDs-&9$d$m6MQ1zJIdvz5!oZ#j7NiO6|^-;QkO2e|4dxEGqg!B!sZ? zo#oQ76B1DoGH+wsYq-cNjG-YAQ>tU*DbI1Afhk0lL_BQ%q9ra6;>o)TWzw{y$w)6^YRTzI0+J?zD1pdULrTc*3X5N2h(Q&Y zz26zI^t3He!$Ayw?C^i}HGVAcdzF-t*3Et4_8<-uu4UI4T60CZb9B?4m#!-r`rBf< z9AnOHEVI?_bwf?{=&rVv0hWokY*@)f>_}etk9;gbZVtL0q#^WnS4CfORjW7s?5!HA zslqw*$1CJFfdaXzOSHpKH=ob=ZSnQF&lT>U5TDU&uEDynKSMpZBVQ$mYX?bTsmj4| zV+}QZ@K@p3jN#8*_R{<1LD^5&Lm_I;zXf^_1q3Nabbg7)lHLy>6m26j2{MDt5g|>3 z2p{&L(SWZYtTnI)5H>Halg{y64yV?7R(4p-BAT|dJtN_mIBjB>k8`?zu_gJh!0HTa zsi-D&)IK3X(hR_Bzh3)JE*;JoGm+Xg42y2T)Vn{f-7dvwx?Ujj&)n1f8hHC!xt>E5 z+7(YPyNL}XCC{b}Ught!@ewDJp@D}nO{X0CTQ2c1WiQ^{d6P3Y2`@5xY!CBG$eDl3py(rj=7dDra`GCtn(6!kmt z&#ONd%hnkw{EZVjXrMYc#MueB(iyYx%gJLWej5wZ{ge*d3AZvXxvrI$K9wdv+78R* z5VY#O6*3MV@Yxb-^!mr?jodmy*YhJhM*&%GDq20&X)xJ_S)YjFKY>XMBt@JEfFLKz zfEW*)2Gg)jAkFf5_codxm{|x;ly!AV$?Vb>_vKZG@a0YnDuJl@i>rR?X7}?^rVUf_ zWqmu}=;@7+6P74ooA^k+j1~R19_I%!;QrCVMA|hpBNt*{X4%WlorgtM|AzsMRV?rE z)t&%re#HuExWs_8^(H<^wpMck6>;z6UD=f`r$gJ94XjW4a47p3+16Oc-Em3Q(& z6r>#B(`mL+2&u4;hTAX2+vUO27VgZVH%7$r}bcS?FOHr=(sg0%!Rz!0Yw$}h;^z@Ze zdU)b4o6k}$H_lyypx!{!+-=(s3727Tzxl1;9!95>p(VrsEy9!Ri0lX* z;FZ*d95F|MHKQ%^paZN{ACgfU=@6*VNJo@er#k2_-qAHd3B|g|oLVHNL@j)9bT-84 zhEy^PrPDpSsiH^R;%;k}*q7s>R#-(0M7S6sepL=b?2QqAG`BdDMceQ>m11aY4t1A@ zn7SGk5T4#c3Hz^G7^l*HM9X1nC*8qvfD*l$2kWA>&T#OXh;e%XR;r@U97L+9&j;mV z8+3&sxv3ROrA{l66;%o=$fhz>0kR$DlTpwbA4@4DK1y64qXUFRV6Kep=b|5QQ5mgY z`h^z>Aq}^zg=)Mj6|;sPGFo?nLRGW9D780k6=_JS{0+mqzJ5culaIJK`9AXX^3mOH zingPw;m^PBrV(HaSuN>Y)nEnGlL3ij!%CgB`tVx=C?*17D}(46!syI&BM;#SMBT(6p>?17+D+xG zUaMbKkHIn)#&{Ilhr>E3!l3IoR#23-0p7d{EU5*WG0JPWHS#9Bi}foZ8=zb=A8mWPB*vwC`P!(QM^UU|S0ku-T2 zgFx14RX3XgVOid{DI0Nnc>euD0p939xSH(y!Ni387en#Ml8?F&>gS7EsF*v1EOYD~ zawj~FcUTB>7bGVS7RoVYPByFy(7q%=;)Q-7607%AGy!V3pfWlL-5*u;ISCEbFoF^s z+ba$g2=55PHWfro@R6nTr`4AM$l%r5i?8<4#VYl*rXKn-aXXyx`=TBW)?s&C+4oHh z^$wgyqyJYyE*5<9?Ml94sj#*sdN!Ch8+(h#K4*`>7%jPq%^f-;vK4ETmF|Achj(u1 zc9GZ=rg3c6?3H~$Si?Zyqq^D4gQ<7EWKj&kb<0pk8wMlL%(32$%gc<#i@}>6W!TMH z{ly@F6IY1+PD4L#qp7X9WTDZCRUQtFfRLv|St#*0Ar#j_wHFLcz?zRMcgk7CZ;2xu zcHY8Fl;<#rkXhgPoPr_cEv@!UIZjv!>|(})+M=+nHr5#Y{)f9;2U|s}znz--ey*;f zGs^u|wuThSdalbm+gR#{C<6LDqd$_TC+0*rGao8~DdM%{Df8?B$vzT4cFz8YIENQY zbDa1tT$^_tkd~2wUA}dzs-5!JF1c`O6+205o~>EF4smJT2N7xfIcd@whrcf4stPQM z%GBj?=lMa|q@2_$>}aPPqL%DdwFMbe57I7nSHO+nZlvM7aSuKG-jdh%{O01{;$v5> zd1Ho#T_vR>>iydFU*;Y(=JHCCG1%TIOEuBLX^<4XZWOx0?fI%mtGcpgi_?{XK@(v* z2rYSFiv6&7%<|4}{?U5>NxK7}@SDXfc4afbMWA!c+HYG?p=22_W{ql}HF}eLR5cT0 zwVcJY&J%ez8))($A@QnFp|SGyBsNy{G_0%QeLp>7xzc#^pHWqM)i(Fm%jsexxLJ*C z#EP9I)54D8du-47u44c!5R*~oww=k|1)TnS2zNE4tj|N@?c z6aw&xi{*9v+hr>5GGx$7v0sWfee~BSbZ4HXk&Dpcnr=Ino4rK;0RzJiBYJ_lx*U~C z-C~@($|&DFh-ovupv<%yxD!8mPHxLccBFxKNH&N=S4n1MnnCUUtMMO#U3AZPdxwHHDn^jOsE( zGMg>mH-fQzvdCs@DHtJrjx1GCBFXMA$jU`=2?Z@5Gt&pQdyK>s7R1pS&{85`i)wEn`5BIOby8}94UX(O0re*){v zp%q9ODs>#V*4&OR(`ILyhS%_Ww<&YSI)t(3_~?L%>%j=ph(+=9+xQ8bfua&@{i??J zFSM68^Z6-`>F=!igi7g%r5NdRdI)a%&@%VX2{9`}RkNN6>Fv7YA7Q>lzwasWvw!s_ z+);TLCFjK>vgRZ_9r~fxduVj5Jf;LB7B=QgghXd_p+P^|z;kYj@03^-p**CE>Lk(# zwSoOr+&g6@b(>KB`~fw}pj)dUA2B*Bj8V=HM8Wa7me-xC{X+L6sG-U0evNdeASCN{ z(EpYrkZwwRh%hk6 zZE2aANhv7NP)#j|rMA2`2XpTD6JpUo2=a3$S()o0Ssb~_OD8+HO_gIP6UiC3p2%=c zthNCHeIxQjP%llpne18WCM%fba?of9S}BgOqv%Ko)$Ky^BU9#*krpVCxWwd3kzPnU zgIsV*Q=LB!R=W5Lt`@BfN30C`;w+h{qMVF9q$hicLPftf77^5~-4+(hNoYT~@loUF zw+W*hQ@=jipi2=*ph!*nFn;cZK|Mf66;Di@7{4-#UkdnpKA~?^H|T`@DDa&--pnt1 z!FMc0IRDFMVU{Cn=7KiY6dj!+^8KVtyuDhd@0CTtmGDoZ_!9r^pxU@ElHtT72hY0i zsjwW)4E(WCQrnv6cHlYke4bF;MZ*Bm%X|~hMGp~*3?$XZ0eO3(t8NBFyW{) zOjWgA7vMl?_7r+!?hiXY5emH^5>liFXL2avc%W7d(a#w?l;l+?IS|;LT|SIz-%1gq zf}vSQrcy+U75Y2rrU0!*GkGmLL2AOAHHZgTYXx66RgujVq8YYrQ^lQJFIjj(`Y#l% z_?i#n@f(tpXaFyZ8V7ySVg=Qy2UNIT(@P~~4Uw@v=lJJYtYsDKUtAabg8&utbEpi) zNG&D>B8+7J5#?dPSwZM%KO(v-X9efjbiD1OttGLN#RSzV#$SnmKYp$A9F1esC@4P! zrJ!TFZa<{`{`sO>OQ)46GhW9NGtZie(OTX;lEXMyKp6uHorX@kyY`>A$z9_%dnFsf z!=hLx{C6M0y{FQ<2?k%gV0Pbsu??er>^(v;Prh&aU4x6<&{=;LDX;jbMHBcQ4AI}B z+7NZSt#!9+oQ`VUNx4@a|2%-1czu3nauXI!pn4TAqL9p< z6j#)FC7`_HbElB*|%a71xHtkG7FjtMaI5QPX^QAg~=r$;i}UCrt|J z=W@5u=B|NwiR-k^7WGFZqhKjCri8_8fF63s=GQ69#NzY37il9EPQpY3!4&?-xScBZ zZ_$Yfu!*?(xf<2BbMvu79cJ3#5*)1&LZnqg@zdzTxhgQ1f^HVOMlMHg!MI0vh!mbY zEU+V`c++_1-g zS(GY?CVj7eM$fC?ya>#2J0kuP2e~7aVT@m4{+t3O6Um{?R`j6D>xy|GlYN|`_zdy& zq4VuL*)cQ6-%)+OOf_1= z`ZB->AXs(;z?6eU5*gc59XGSqu~~YBN_CMcKTf-`Go1#loqcl4GMG7F`d+?LPmi7b zIJ(hFFf|3^8FSjUpVD@93{W{dTEh}5oVK7H1B(d~*bBF^#=osWWm%7z47~Pr9JOm<5jm^xPp%0f?*HWLkXYDxalEBL|gfD zr9E2m)RO%Z;t|3=%ppM;_eAAn5izM5P;7$_F3XIjqVRUx=B{?3785^Q3l|26(SlBS z_&rYP+uK#iAGb(Xk(QA!djR`ab(0|a#nUX4Jt#NiJOSd%e zQ%3F4#Z!RiOZPo;hp71l6P2RwSo6l6Sbu|Nj#bf-cU>By**n_q_t&bZFO@O4 z2K=NJ+zPy2iZd4m)^|T*2>-558pSI7$KHx$)70XRS@)|T%sdzd9$hOBZcr3uE5%aN z#w})t7(_NTCee5L5T*g%cjZl2{efF!AC*_R*FMrJ6qmI9?+5>YFItpYZ=nq_3%^ zC(gZ6^)TDy)g$1dBN(Z@V?Hb;AI_{llQG`QHN5A#7wJD2BU}-5{7;0{Us;N>6C9X{ zt)2iSoWh9IX1sbT!W;N4eK(R>P|Y?l7m1d5~Zy4cNLsB)Nix;UTbg-^tf z!=wTHlLcqfz<0oU2CeBid#S007x4sIbcX}t z@K@C8Wa2Ueb6P(Jb9lf-p4lTAfwG%7*1So!ija`u$|7^SRFIMhSe*}$I&~X&;wv0m z=(|+z+%NUsGx$8?YV3r+blgR-W*BH$0EL@yz{GBdw;!PFTNX*UxBI28I!#P(Be*pt z&J44@`~FaGbzI6I68C^38#sI4*yuxwV*)L8C0)6@Gw~^@W02+IOpCS1STy*rC0Nj= z!Bo+=vpODJj>`x98&5iz_#9z7$iE3-tM5K%loOly<-Hw4C;Hp~dJ!S?IoP z7VDy>`i4!|(S>!(>D!2KaZE+MTq^o-82P=vuoy=z=b-Fr%%l0DNZB9%$w76pDb;}^ z3f#gxt3CYrh+fAan&IVh!9-&I*tvX?-rve(!w<)-{hx3HxM@?!Et__qdEI1C;mDVV zBX@9PbDe(lB^Cazhhc!l~C)Eb7U;na}MF=#{17;lzTFc@% ztLXmgD3DeF03nfB-Sv{Y9H@0B2$R6K$$<}q-unfy#M+xcoNm1&td8A?D3KS6rw9`t zfka~ciSg$ZTiI}49$aGcVqdsWVrm}CoZhym647|=tUxzycG-jziim$@A!nn6CSdT~Sc=50mW?p83r(YsenY-E`ZsYp15l2 zBYKSnyt=#uZ9%vWxupCa_zgb4oey_EPX{K>rx{V}8%+KGGF#{3-JZf3MX~vf;vLk? zB$?U?b5fALPD)SDe+OtN3>A9(iu;C{$EeQ2>-$tPZfaVL-%4B**-fY$>zbUP7Y(&A zL(g_nayUpxJB|}iv#^S(S2^G8w9xo%DJ)rY`1w1-$J^D-u))s5kF13XU3~#)(P5v7 zEzuCBdLN?x-&=+8q6_4{E9|+Snp-cyYvsb}R8gD@`N70FHe!9DhidBNa5QFU10c6g zrIW9up4vYD)OH@9Gr8_>+bNBsz>B=}ODZJHMVm{o z#n$KU-xi1QH})s~Izvvo!w<}ii>a4kimy^>ECKxYZ~co#P?+~qP<({bne!Zz(=d{l zJD${S5CdkGd1H)pwS9F8y)ao}3IxxmBlyCFXr8Gz)`WKC>PQtJmOl8sP2fw$7njRZ zb;Vt2wTffAa$UCi`h5$&AduLDg&3*=3Mz@=#3ojUg;nZZL(r+gUF(DxCR5sf*3hBl zQnQK7ye0zb^4WW zzYb?=!I0QbTO9gO1U-UmA~~)htbrp6pO=+_aI#m`fp~+jLNQ^oNqe_LJiTWqH-lkh zFTOrcrS-!RbE?3sn@j*l)%XE5lSNj(?xAl4LdbcQNFtq3)x+dUAc~kmrlA6)u=v(O znvLt9FCGH|{5Ti-lAxa8Bo`1C_V(D9v`aGGY6az`f6USiz69>BIkO)(_$`WT=xzoz zZ0Vq0NadiWHt8@k`1gs+?yGSe(#b-J6S0m}AqoM0j5oPkAxC!n-P42^?%(Hau zFMawkE#H&~hH8$$>36P@70D$nB6ejAO7>vNDFa7%^psW~k@QDEPedT{d)rOQ-yH%> ze*u<{-m$yHkT2GbMW@SENd!j~Lzdg-qJ<$EJYHtj9({mS%5sjN3qF(oNbxgseeWLy z$9$25HkiHL&&9Y~tE52kKjTJO`JyoA9Vi1XPVghAyO0)5+_*a_hy z(WkS|TeiD*vA4s{t5Jcvda}{$aonA!9_JA#vNsTWQn7El2}tUD?sq11x>SD&4Ss5a zX(N4atBm}|3zFJsUmR>Df%+rH&Qi_|eOV*1C~`@L%=J9IG~e&nhCqzm+t!KBbr?gP zWZsRRI?M@4ro#Oou@8%eQG*>1C{`gaKu!o3p333g3un+vWe{@Gt8CJ67ngUb*>IKJ zrDt<356>1t3H{gYp9|~+NsW!{40^B_UFeLr{R%%3jTAchV3d13R)@bC{91}_WvBUT zI-5W4f!H>i*t->SDjx13y5pcmI`)b(OUKCYAGgW;D9~@C{hu6CqLcAC`3eNF-uUMPXhfE=_q3KxZoZ zl9|#o5uK?H0`U#$U!_#t&+E^IDSN%Z)gP{@G>IcCba@M1F2qRs&_WyB!&nv?K&R&W$wF!bX@SeX-ehr`q+LPXmzj@s+loHz=TRwm; z?A*8B{&ObedKH-2)YjGX-&OQcwok0uVGv1@;X}PzvtZiSu=GjH$uO>T&7hR)Mqjo& z%bd%PPIf)2KbZ3s5KyYGZ^(bk8HcOZDzCC)K%R>4cW7+&5g;TUQSE=?yQ2$sgGwY8 zh)&nf+vP`62o3%`2u_?3xgO#V{@`Vf$6NBupWvtJ&pIC7(m8Nf*tGxoRn*g5Qz?eC z+h_R*KXjcSf8SNj53xE=it>K6#=1_Um&@~s8e3F~KY-$j@t6Zv2RlSBYheSY{iE&h zzq%=IHGl@9N1Gc-dlZB-JmyHpGgZ;>D1uqZX(sF)8&NUl)bT5FVaE+A1CMTl-yb)V zL~ls5|A^w0?|M{@&9vz(0g3{4_*i9VH6u}rpcAV7D}ka}m36j|=w7(+zv~Dv4LfdR zg``BcO^Q#ygjW$YABa5I>As3H!A;QfR|M&!yc zJ>Wgl)Sd%Zm7uGu@DQp6vqs%$7Vwwx1^8@pc(~^no8tjaP8vLh9fJr2jatgIwm+%x z6WAtZf*$UlO8MSU5=jL^c@rAD+Fd67Z&>4eLVk z4bM*at~~pS%7F0_6g!hdGKw?tBgK2cT#Boe8S)S@v#1t|Nx@W%7fK0EUJO|bTSWM8 zAW?-aS-sw{=1wYY1F3C_77~Hv6gCbb)*N_lK#I6ug?T^>iAbH@ zw!nuH!Cw`Qg-{j8bN@moeci%Rs1Z3OQ%nmoha}hYr6Xl-U!tVv5?P5C!m69jcj_XR zte@|hVC?ObStf{&PX!7@rU4;yQzo^EQiS$)&hf($U4tG{w&y#Ok(+^iNq)CY3U;22 zU%!Su`V)d5-c8~ff{sgFPe{lntYqt@4p3_lQVq6{3_I%_#XVx9XaRYXtVQKv5d%asLD^?`sps6vCynLQf!N*EEoQygs?9WptzIlI5d51-14Jx8J7F zp?Gi6_Scd%Vaw&+Q`jS9ZK$a35(@f)6bi@(&?@oUC&QvzXHCu9rra8)yt0Z%%s6c* z(g@l3D~arc-@|_5_5o&5#KqY_#}%_`z~1cfy(#W1(L_FfMX*WaRjhgGh+H8aa;~v+)0AMi- z{EgWAXjC}rU~=!}qTuuJw;*0t5obMsMPvqxa^?vf|?iqL8S&Oouu5Aj5%0EfZmRW%|Z(O1n! zn98K-Q-58GBm5}3b=6)clsuJ!a1Nxyu3CArLH~KhpO@>~ptW4R^9)hlKrh(SU!?$> z&0>AshJnNnL|M__?JRv1u^)l+S6MZT`r~eEZBavKEmQh(7k*9Z%&f9VXT5SB|9vLq z`+LVF$4}GPz+a64?^1bRhhf>xk!~?mf5~^uoRP!P8+8 zW;9o|(x!|SaN7L|S5Z!5rwck?z!U1*^1>(Se4(bLcJI?f5Yq0;)xxM||K-rec0bOR z0ba6C=#(XiV%+06TmRd=&%47;oQ3Rv(tg;PBT>sUZU0?_>TpZJJHF%BZICHmjI7!} z^s{8l!OhcE#;#iya|(ra+`TH2{p++!*RAi~lnFBBIQZoD1aPOnh+_#d^{-wSDvY8C z+w!EKI;!E>BNipCn2qyh%|;KG)pyIFRuGEhDxp3n?J@nU%;XiU_ps8?OMly$7$ey- zlr8S?uK5TLd;TzqyR!`6&gF|t6?eaK`f3ZhTtV6wEIN3+Y$w6!wTuNo-60La4K6lpz5I#o>0Z6`~Ry}{Y z_Gkp5?*>qQ!5lA~AkVU88Xx8a;y&l8Fc>#MU5Rhsg7eUM!n@?B5|oHqox*5Y_Ou87 zzViE09zHc$Y~%x5&QOCMDviVb+AJLUE(9~G6n5b-(A0h3L1KH)-W|O#e*a1=-~mu# zIsR+p=$JRb7d)eR8|oZ{IA8EiozogN)d}XyrDyagZQUaFU95Ym(A^d zQUG@8REdoe0Rzvv6GJ(+zyIP5SpaPHe{HJNLij0TJ7SJ|!V%hu^J-TM6P*AZ#$z5v z(I?z*v?y$vliw;*P++}AIa>$4;aou6Q4^& zplNUTix=P80!UF&rpciY)~Rr6Je|DPT|=REP|Tm2$qLL8p~*w;9=vxG#e%n@Zs!*K ziC;DaJbtkfZfLRRy4OmRDysZQ9=f>c0RX}3&a{tA@L}lu-eTz`B(>9OZcYon|U*=mK-*Pz&jmvgp>YOLVs9@T=e=|5&lxL=ZU!cBsI8@Rm8(tezI z_XDn~##G-|8}=TJKfe+Sx&RKBD3AX#w71V6KC}4<7d)b<4n`!XhUK!LKZ;(2ozwFb zGSAQdK3!Yh+p|++f0{g6OPiwnPwqG}UTUw-8oE5&d4h*DcCt;#WP#O_Uu+=@`s9h# z8XV9a?Csh4R@LGU$Ytp~6lmU(rZMeh@hC}-z)AhO6dv7nXk=@zaEB-+{#W4^ z?`hP!mkMUhF=9muxGya1;gq7ZjZJB}b*&21S#!s_y09{F7PEqUqQ^O4ZEbs)rwv)w zlDF(nn?26oUD$3)X5Sfk{s*W)SHJFzR?FyuHNeMsL`EiY_p<-J&HiB3;=R!{3~-O_ zow+?eZ~2QWP9ZU6t?n1=1OeHH9Pg_D;rq7Uq=GTT)PljZTu;p?+B}pj2-Fcx-m@Oe zjfgZnOkmWEOj8QYhoI{>@Yu&c2!{_H!%zOiZ{V&wPGM#F5JsaVFekj>_uhs1Y=rT6 zf3PtjLlOM*{7!UNhLg_&CIo5eqnOUAR$~tc6R8EPViml~x>vdqL0< z^c4&8p1frt;n8958?hM0(K;$_6!Dw7P)wsFa;**6vWc8C=JQ?5=e?It7UXkDRcw8z zdvmDIGbi`=WEb&~yHoXD2C1SU#iJkjV7&VSw`0DuYec%<-1m%tXE;h}&BAf6;eCun zi~4gR$f}dp2Mly|+vMbAHPsp%Yf>4^>?$Mje@`*ipdlnErdvJkd;J<7^N+p|x!Z-P zi_54ynYvZfSiuoAEW_B3T8shLCM3;O92CT!SY~VkRtJMs5N=NJE~ZbVE?^VXZQL3F zJsJwxtYSQ72qA{TuB{;uMV3)Wi*mc*Df1d?EZ1Fjf*e` zXAD}-%`lEa9luux_lBN^yn`au>(0q$md$X;y*-0bS21N#EHAHMadEMns$sq7uU^2* zDxD4yaOGM=YcwTe;%;x9#+NJ1AqJrsk7nfaj z3{QIU6WoZE3RZyurc$KXPQzC5gkoTRtFfuImL)-80%~SKGOU69T|xvmC;>nyb3tP8 z6x4z+IRIClm*6drcJqi*SM{R4xw*t|tJRDkX~5gDn|-ECfr-+P~LqO4_e zhgQSD-CRgc;dL2bCHykV1GN)m;H}|hF{h1X(_( z1L6ij?jzdG=haP1LpxE&Ee5wkL|i9hB_w(IA(}lvIi8q$^f{y3oED7)A(iQ6B$iDH zS8^6NDRwjx5|m)n^imm0nF?nNd@8d1(8m~Vku^EiVCT) z!AuA(%&?PTgqh-R=j0Ms2Ig$Ogbic70L*50qwD7Q?(hCuTz>g6{Mswtgq-JCUR=f5 zb0=`+Rjc?HKm09NUF&e}?44LzU_d%Yy4lN`dY=Mw1dG z_qO5uzVxMIP?enZ6rv(_vax>E=8Sm&B6_N9yb(cihaz#34i(p~Ao*CJ5s^Kls(SE+ zS7;a{XldWkU;_70xK*s@BghR72e=1?s35yimYjcJPG8w46-KV|ibt_ZMov`JppO2BD!T40 zwssU>^EF?B_0E5d5N1&F zVZ>~xdJ-38A=&h%VmqNA&PZ@lPEe?GxGhm>1xom07P_=};LwtFl;8mqpM@z)Y|-$Y zNC|F+>rB^9*h#5FR_(F%=qrh%?nUB?7B)JVH=~gdAl_ zaVx8CUN#bv2TIjSup(lc;ld2oRP6qAz$y)+KJ@M%cxJqB`f&L%BI! z`+vJbbV@*KfZT83^tqexrBA*;jvZOXZ~f+*@#HUi6dwApt8wDwCorA}ng-~)9>i)V zUh!EZFgK9SMPLF?D@~XL*FBKNtWjgci75Kq`%_h$G%{pW5~Zi8&rbh1esz0BjtVXG-EHB!~L7P__r8tN8Z;rMXNX zA@YufmrE)TH`KK($K~!m7bDo6F+eNWvV@>e*8^eJW%SYl?J8)zip{CHjzVn)tYJY^ z@RAS?#(fMHr)f=wa_ulzW>arMqjyj;}k=$#5>o68Isd-8<)WxXNqh<_(X#}D?2>$_Mr$i(84}%mw z_!JaGvzTPoK?K%=D@PG4Ha0Mr(oo%&_)pjAXZ-xR3BaW#~o?uHb~ zRr0b1P9=sXE#mE8k&Jma+vCJU#AfNvjE2)d%^U4ahPpd=xj!i(?+Qjs2XShX@Y6s2 z3Vim&8Klvu4Bye9l9T!nVP7#mgCiJIG$;!>oWBNGV|3rh706)`5bjmMlq8UYq$ruk z@FwK=&+PX^Z1%`>M{vbuYxp<+^7**>*a2)`xD%A5W@a(tG)$j|vt;)pQHmmX`okZ< zu%3E)o+REj)M#+}Mv&ggP*laqaZ{MeK-<70RDi?{nr4jDHGl$rcH4J008v%Z_x3qb z+W^vIYx@M6W?~Xsr|Sf|AM-V%U0Vr@2@rFWQ z79K+0AHH2(vuEQ)obpOehCTBxr?IUOiioxIX4swj3a_ z)GRobV(mWJb4wWbQ`Rc3d?1Bs#ucv4cMAQfkKTokeC*S>`tpamG{L(jj^D+22hc#g zs1^U&R{^2`i33i$LI&LoUwAyAtGXY7wbqUMA46`+6fKJ9PtwVl{?5LsjZ^bwl21#%1N8ys9a4(2iX zUO?2=R8vuC*F*_93$hZ%O^Y*U&f()XehTC9#1}?@XMs=;*?Lq3UqFQqsRqnigRw{R^rIoG$Xuj`rIaJqPehdSHDpQm)_a*u;Yk6>+e z4MH7=-J1h-r!g~HXE{>Qn*;!wY)Eq`U75g5i#t!8Lc0Vktga&UbEvRqkK&jWyd|4F zwRK{eRNhFqeAl26vp>ANE&7+5O_uvF*@q}g;pnh`ffQMaDi9!~)T|_N1ImJK*GOax za_~6j*ouRno;j-%@FcPtyWjgNK%hcQwUZHcY2xWvIQSHR66L0AjFvC8acvnHXvl;2 zKv`(V4quJaXKz6_n`1m#z`Wn}7Ft^#h=`N{uEwLVh1B=iy(PRauT&;r*@USUnp@0v zfQ4oahYnl;!US1hR9V@Jv5;#NjJj+lHmMn5X?X?X(Fm3P0*An9Km~RRH4VsR>dH>v z=k?yiLZ!jUSdCSK!AagSW3JsHi4g?lsa4=3-kT_)kSaXO)4~#~PoZQAp-1ozLP(ZW zGj`{@Xq>zht-~-Q4!b^ha7m6k(BQ2Sr}DZ$f_4hc9lqf6uE87s?K>*Wj*GH(FZAlX8lbjp07#4gwdKYbGkl3CAh!Y(?W;H0b6G^b5H9 z-uJ_(Ie>01NHjvu{vO0o=?5Sc9PG2#&AB%}Mt7bfCy|Mt)TPKGW0s=AWo`o(tw-;{ zNB)Zn95O}5gMy)$>ogaPAr`T~{R_Y#Zro8RIyWfyoZR|ufZMy!h>EM15i8ljS3hT| zYEo1cc|Jogb1P!pqn^F<6ZlA$zCPbbvC9x8LQ1UzzCdacX1g1B!ebtU!$%KeW7@ej zKyutdimpZJbExWwu35~baQgtHGSQ96-kE)KGBx1DWhz)KA_hu#hn;Z>53cIAhvfPg zCFgexW@xmj#lh7HE<3z{?ai}jIi3>*uc{mpI*fMZ8STRo1E{UJ=q;1ahqOJKkh2SG z$iN-)kj^PsbWTX|jsbb_V4rx+%$RpG^H~5vdcl3}{Sa_s+;#V-K{SIRBNxI>03-f6 zWzd%MKR5N>;B+ULrCDyUxOf2fy6y{bZ2da)^9D&r;M8K?bpR!|(5u-{b(FU6HJOaC zxG+J}jI3jmbM%9YZv@22U)+{{mc&Vp_((2+hPNV-1|XyTgbXwz#dBfs_jWnenIe!i zHERj}1=;3eVZV=rt32T3Y9|9b#tsASi{=NiapmvkUm?M<`cjzW2j(s z$lTSPwX0o>Wf&tS>~_0YY8hYh@N3<_+&gCxpmLx?_F7qe?4G7V;)~WZ2w)#b1lGZK z&_Q7Kb|b0}Wn#A=mRu<3iBhFP*R~17ov^~%z}ol%l{x0q4X{qZ323N67j8lh=ts9G zI(@?px32<(eUCIa31rV{SZluI?7qsEcRw;;&;}h!L4_*^B5LBm6*!GCpZ7@Z5*gcgY# zbiT*|hV;E_2dJj;4%E(Pa~wZ@3{U#fCzQ=;~sm5D16}D0GK0BcA!nAav?~=pD{pCspVMd3{-JhJRc$B zP~@C3nv5}-w3yH4M(S#kbqhm#?K+I`xFsUvn^0p6+MeCU&jr}3m^6&O-$A#z>z_T9 z``^p3^;>m5(C; zu7&!mYCX6_4ZuJ*@6o0ONS;A8;o!=>abWFAOJ^lJ$mQz3;uXo|kq-Jsb%T>ryVcs7 zxhKr0j6|)Gy9WCu04|fBSL=5;(Ano6ZDOn~Pka*707ofm!H`K)Iqg~M)N7PriZYWS z%92o7Cg1-;V$;eICaQ5SJ%gh(7%%5{dpM(^_+i>>rxaQc^5+{TWBhj}v`_k66iQ{a{)`-0kEL>bw zEbOCwp@kt~0a+?0R7^Dkdx=h*8lS~Br?u6YVqAdRDEeAbPX6rza`(fofWtPfL2~cU znM&pt>qN`pP_O8p?YjUGW-$=~;Wvk!FcQ??lJaN>)7Je+o>{455}}l^Gu{?kj{O-< zS4tol!&Ln909B9#Rb(<(t4OhP)ctPpC!UqrLQh^DIW)}-Wb%6T5%zm*ErSIP^VUQWMt< zqBx5WlDJqg`!GOce*mcxaC{zyx&|mE9?*@09OKcvjEl>b>Pk(s3LDsgJ#k*VIPle^ z#sW>$k;>jXhHqBeB+_f)>W3^?G%V5WL}k>jiKv3JZNk2{hhltudz#{b0OF;QsQDa{ zfr*-pi5Wt!LMn2eJtaS_o#zos%B()*WpJLvK%Q7MtdSDSAelxu{8plskG!+jU<21A zdvLD9krNZL*1R|sU4jkemRVs|!hUw3daSeAw4ThRK-DD*i5?oMU0YcUa&bKbS2uY>FZ z0e~50+MZ4-fhNGpu)oV8s1mh|gzZF!<1CpJELt9DY)|1T%#qwX=HTgkO4UX$TDV$O zjZr^PGQ0WWG;J%(Y3#L)SJGCL+f=FF@YEUv4I`yUeM9UOrymhA8IV^~bSZXpz4U(C z$GytM*PwFTC^gFnWW)q;ejE!ABgtmRhgQ`riCkz#;dsL z=5u)IFZ?z(wstK@^%iRSz_7GF{!%5B6P!wmS_WK}!M~Q@3&7PdDg|f8^Pl@Ac+N8( zh3Uo|(7AP+xcY;nQo>YW-#5=>b#F3=wQL~ZS-Qkl7h+J0+uM#&kAE;g6C zDTe`VHq)XE%!$PxeD}a6h9D87qPdFFcTdDdRvGm8gM%O4@DR&XU%*%~0rXVxhdc`j zR$syV%Lfd{;_O)r6`FIEQKQhI3ZRD#&sp+N=4%D6dl?*k7I6pTz{cqO-T=OU2i}-OG5y--ayWYp;zNH`yf;2bp#c|z!5Epu z=(CkhTw0upCT1=68Kcnzr%s*2O`p6OS#zZTJ6OqDS&}4XxK_Idv7dvT$T3tDgLPT` zAogS=T%vVRPX|Spxy;vq_wu2@y{^3%jvP4%?ae4hU~EB~R<-A$Ruf{SGNHV7N{pf% z1UYk}LOBr&?og~HtOimSZk3u_6s#6RqctrsA>HnDj+3WOV{wtNw7iVn*|e03c0f5a zsJ(Rzie10mpU}B+&DJYu>#A9V|0YG3M6ozZkogX}`5g0khbDpu6BORf8hA*=J&S-D zKATk9sNOye9#HWIY3v!rx?+GD2KN9bEew%;7a$COpwh(Jia0fL6Ik)l;qK&|j1NP|!j6-t-BKMs1<*mh5OiorX{GE`?@b25^AUsk=d450g{% zp|k1&lCUTfXCVfcz1+SMrL1k&q=f12E|%6-aP+bx$X(|fo1K~IctT`bB}fGFZ^&HM zY(^|$Cm-@K6|k3dp}k;LWGA};YGS9zL_)^rgsEO_jEI#ag1(z$VY$KMzT^?O<+Fc= z-Hi)n!^>d9$2viPjDkIDsFd?{($F5F82q)u`+*3UPJu}SJnSL&#p0yJbW=*%3fQ5< z#o*XpiQR$MsQ%Rpq)@s`$!Q%rPK@jgm@=C!)@iIQ53>6tKty`)?KCa16NwD{W+dkZ zc@B{SA{CO_3LXvw@`K#m(>swm{3JGZr5e^&+rTH&Qovl^FW`SO zS&5TpkcyUhnYl*#Ajzc0=RH{MJQ|HK9xd2CNns>1FuFxZ#baT`inf%~&(y-xr|1Ov za3LE65x4<609eZ%;=R+ri9#S1{s~g_Dp@j`wyEDQ>gZ5y#zZZuZP+NSs>n?$780$~ z$P5d}N~MEk19bL^lK}YXZzpNa*@p6pVzzw_U-van#e*O40PIYAq_!=kO?jsx26Wmv zGf3=&MwstNU5~Y#tze@Cv*2MUL^4(u8(ee68fIHI?51s+GUGI41{FE0YmGt_NHLq% zf@+i>8w)uB~+1jQ*au1DA`-~ zKDsS97U3`+8-|Ho2JaLf$sh)1^O+5k4T-&E3^|+tJX@>(ZMJv>@(hs;^DTm-P#xxy zeZa;EBH#xuzKy{!B)^Zj&xQYCT@9%;ZzwqLf=Ka*FT5P!ROWpkPH@k(xCC?0ur&TI zf?ilq8xWcUOA0_+-93sbKn(QgXBUw24u~@}`xXv`YwwsR9Kfp zj9lwt5V7fn2uO{6`=&`~nuKn@xmw#I@L7#f?*^e<;}joQvd6#|KRtq<5Dz!fF+|DX z2}3F;5xGWQYXI?-EdD+~u->&8<SpO|Hk97 zvx69*7CW;J%9dVd$sp;GQnCTW(wW;5fR@bG&r(V)L!IriwMF8%Sn|)J**G9gWm?8M z6LxSEIEF-V_)*2khik1i5k{V(@F6Zk`esIE^E1AGARU zPUugs7RwL@5)V6+xkmFT+%N^>C}U&V2$~_Hk%#Ef@X` z2=<%?2cn{3<4J=nuRLDbj~buRe?RaApF86%#TY>2e2PIPM#E`YAh71rG85k`!`Cgi zP4u8qk@eWTi3xo_NA5avd2+ zXn|Gl>7ll}QpgPm(>=IH-*+%VZ|&|xTi1YVDI}S~kc`s8ZRjEurud9f(ePqm@7YfB zpe0O3t|^dg8F!ZlBAKfkUgIWRc=-`#$>oY$J%fc9ve|t z!Abc00g9Y6uDkvkTz=){xa0OavA({BPk!b${MCCug6BTti*Rn^6q;Vpu$`hRQOQJS z$9mCwd^4S`Yj{XgN7!i<)jlZ#@;Y2lLQQWga`R`@6G zyI5S91NEz5?&N@lL$xU%doh{=_fBL+*DUtS?V6nY5UU z$Ib-^P|+mQ%Ol2>m#t$VCCug*u+TP0>Z(-o%o2d|2%-raI~^7e9z{>f_~1uBjag6V zvnfZTyo_~R*SUL_c$-(fEJlP+t?gj-&IsZ;`+bQ!Adb1&j8fpj_8cGh$S3gDcYFX_ zvyAn_hkz#Gw%hK;lb`wkJpRcK#pd=+5Sj18;k`;AZqP?>zu1xm;YpDZ&P`GmeN2Yd zT!b@$jt@EPaH}wr>Dp!&iO1ru(m~6%YP__S3hrMvpM!Iy`!WEjI8sz~u&zxC?Z;>eN1SUGkzcDK&sx8C^Ic-0^LC6*>5G^9w4cecEQ zVD}7>nc`c%?#cM(=R6UjGYEEo7(l~r4x_-VW6ZV1!7J~B+fMD^XMXNC@aONi0YZXa zVnl)Zh&v2M7smSXB_Eit4xUwDoq|2nwh)cA!p22WX~O@#cWh*3eHjPWCYW^{PM$i0 zhd%lmyzrks9kc!f`hKH4_5Bqf93*Qv5@0C{OWX%R{Um{F7RYC`tAwwCxk`(Jk)ZG{ z7$E380J?%uQot|od;4asSOto3^XL~~(55=Sb7^8dw>&v_+#mc6>lj9tHCf9L^^5cWJOVy3hu8$t=JO=3)^yI5I2 zh@biCpF}fi@#@$89(HDPtgWtK;m9&HcbIFBS>M|MBT^WkEE$uAvAJ~?fA#*4;W^Lx zQcRi=wA+S;;SRURXfPMXj2bK*z8W9+52blW(Ce|u_&f2F?MQDtx;KjWTLLT7LxjXHUBp2O-fo=Bpxv#e3b`7Q)X|#%@ zoPebpz=1pmfJB4N1C`eg9z@NLJz$6w8#VGAUL8ZoIW!zbj!KlQ&6N`RoQv;*QUY@X zI@@?=fv4_lB>vi=`m1G&fxA*VJ8^!M*$Av1wQLGwrk)4N5Wj%}xc8Z0<#klbm8vqP|{+vjAONA;RtzNYL!lwqyL8 zPrZ)Ro-~X|V>8vMUT`6qBp+w5?nT0lJKrTNuC4$`n9sH_pYC9sno>U4bgib8BsmKb zCl^1N+`VWq0D!V-EHq4Xd;%1)-IOQ+f_GV%HE?kmJUH;+{;*s98DefAr8{+(cpk8( z-wSqUMh~VjGb8W~lyGC64|DbMjq12dq+ZoJ-jtJu4TVQ$E`xa@NAJ5i(2UTujM;P> ztqNZB&EJYI`?9CvRj>bDy!2(ifUVsQqlIO3S-?$;7{i??1N{`*Pmw7VUr+O~$Y@3* zKnS~Y0*?=2dHqWK+N=K*KmGH+iH=5CIeZX1^A2Y&Y~aw5C49lP_kptit`pTd2vX_Z z)PY0Aw7mob8f2yz$jpZdT=nViFbhDoZcJ(zqe+9qM-Sq<>o3FoA9ybuIKWK-8L6AbDib}W08?sY_-H9SSkJpdg;d~Bp-A!Shs*lYhq*Pn7 zg%=TJpO-S4dvmdF6I45#**J;ouDB9k|Geko6|ej)Y@IvF zc>Xs(1D9RCg09~NwD+tp7e5jzBuOd`hH(JxNdpKq>kOl#nAA0iP2~T zN68?=F*x}udmIF)PA)% z`$nfB3}PI&XoN6M4LA*uqELVCxoYZNYUnca06rND=w>rC;|Y3I%)1V2>zCn`zwzt% z?(h0uY;J7g+0XtO{PHjT0u~k)FyGliJGSZK2F8W0oxNr+mOY4+DClm^o*fLFj0*4< z{>ds{Dh)02;&wViK?dKg&tCd^Mi|q~;#O;DG%S zAnJKeARZwL7ul_)^HFXINbM`x>1riO*dM57&myWbR0M-mT!mb^JTUx#2#^Iy6|Dk! zcMIFw8<1I7*w)cbjkXm#FlV3L`r_gB(~d@1TwO(4SOTXJ`mE^Y9o7yX#p_=4DtzDf zem|Pj;Miry@Tyn677u&aL-2z?_(SMsJEkTKfsSNnWF|}|V^cK_*3~d|FB}19?6KB~ zmzIj>Qq&fH!hmia3)Ix7r3+sJ?Eq&e1}9>Ga)6VDyor|~1Mh*_s<68ktkNP5s{vR6 zG$9yq8@8Za&t@|aWsH1?l0=Egjj3`yKIeW`Olk;bR9gjnFx9 z8ljU0bDiMev8!jtOMVh>f6onAKXwE{gp+4ZW3tlV+y3dZ@#LpI47& zgQ)|;uVTPY`CobE|IXhL0Zsbb-t)ixvwaaD;)?0RIgm#G$I{nam7G5I9^U4vG6OFI zT+%IG-KjVG%RCwkjbaJf%`u^bH0yBd4IjgaPk$ORpJ7DC$yXmB8*Mms7bKxvm3rjP zqe{prp;=zWq06tr75Bd%sBN&iw1&TW>pSs%|Lg~mv*Os%qd0T=6xLVQai8ly&y|aH zo@PBi8_Z{O+;!(k%xCkWfhcn9ka=U!e%*6s<*JMgZOcI3hJi#nenY$r2;N62MZ`~U z&+>|**8g2RdU1GkXbDBN;}Oiw2r>$|5#Inf_U}SwP%H{8FD>Elp>-rSdw52jIiV^* zA}_ycwxjMIPGSsF27v6HF_>p(@9W1pu!PjK=zE~25hSf+`QQrP|A{;BZ+`sc_}HiK z!l7fwAq<>4b2kngS;F`He_nuxKl)xcef9=uzk!AaV;JhKtDWrsLsxVu{#>v?(L_$r~}jLprHxaGtteD<@S#RDJr1l;hkPvhIa z^*gY=J;l)@$8hTOX-s#fc*#qC0$=^iXJLE$lucS9t1G3q4ctyz+;!(&c;{dJ6;f)P ze!Lg33QS;+yOjGSa9N?zt+9JB%nmrPi9!3&f^Ce05wDSBW~UAo{yq zUAut0PyG!@x62AE0zrS>@<{&IClddI>wo-)`zk;X0qP(nxvO(@+f~E&F}fWXq^)r} zhHlFzGiuEq!lqth4x*UpMgxF$r}*T1-h=JWeioOF#+dXO&AbO`M$;r~t;MWIKfRe+ zGhT7$DHWR+k%cg6N0>||IMemm-JRnv{^(EeQ@{M5aq`4T96EX&8=G6$-rm8BU;JWx z*Y|ukwztm|mm+oFfhO6AK>+Bw4k?-g zVBnxm6(=)Ml|Vzq(r>AKx?17{E7hk}bN%xmjw%$?RFYJbdDw%6_yIxW6QW$;ga*@{ zZESCE1A4@TTZ+tTHt$fdh;<)8&Y6MxljhPHGj5Bm53>&&@2K=NAUB% z{#*F@U;I@F8mt~ZimmA;&YU}oM|{!e;YHu~Y#hFfaPIt#Al>m6@9`w++al% zGMgbeNJuafkunl4PC5n`+wa8jEANFf z=QpvjaRD#+i5KI0{@Hh7XXiXrGsbOWuMrE38Pc0$7N_bsZ5#l5e*%FrAyf9AdNjtlRP8P7SdmdZUvv|rg9)WNBj%T8soW}`VXC^G^i%w4*)5~Elf4dZ9)T~gEU;Sb18reCr)78^;lGe z?94Gq-Uo-kyKTq}G|4H`#it-@9-H=*3Fxz6R|veejJq#v;b;Eyui=in?#7X$m*K*> z^VrD}1Wq32r+j5a@XyJQ=1wBe&AI#$z^y`OnKI~Dt z-CvN=G;j;};NPlmi6AA}o_X$VvUk%oP*&u*ph;ZDDZ)us(Z01m8j!J~JK`OjIx^7i ztAUsVMZ#!l8Mobi4xhR04jepm1yUlM-8zT)>@2?K*SP-@MZ@-95O% z{t6D*F99MRX-e!0hewAp&EN;TSU{14K!lX zT5|jJGNwHYX&n<$R_j=BMm-Ge#HZ9^XWoNX*Rjnle(sg8z~6l0lQ?kYRoIz#*gSU* z-~GMci68kl{|dXi=P{q{U@{(~?>ke3hED=DZvO_%x;YLVIf6$&`iqcr?|uWQi!p55 z*xabqTaHD74lP8op`mINa6!0&1Egv%G;pT!d!5XYi@iOZi_xxsl0i~|sKZ^>4EHS{ z87LDGsXmIaE!-!O4g>J4ioQ{^kDY@Dd#q*z%eUIMTq9SU~ zaM)s0``*10dQE6X%UE1k#vFwC&L%{5@iou;3OxS>Pr%Of)7afPiNsQWe;)>hxb{VW z7+y+NS93!Ucn3i%9lH2N#tPAjQJ6NtvIrvxdI~Kyg$`#Uk!+`C--B`nWixVZU~a%& zHc^mqDBLI{>TTGhEIXBrshEke+0DQ!2eCU|z)OGi6?pH*Z^YrtufS~HVe8Z>eC=0# z1zz;6-;8c|2c$g~Cu7@~d0+wW-4}9kP^TRh7ACmn-uH6)w>wjZyZ68ZrquUT;)Zh| z!r#!j5p1Vn&1Sg#_(6QtGrtV;?pEyV-d*?>dqClRdhNdep|wPi;&=p~+h}b$6Epc& zBQ-0qSja(b&SDut+S_A~)5>Vn^D?+av)hj|qf(1$ku+zxrFhjdy(LgIGU$6w>$DI&%uoc*5iHoUeKYw(mZH%dR|D9FGHCXFrwM zfj0X>f(C@X>(I&EYXvuyb-6o+r##mU8P?~eg)`8C=s+|L$7I5bq8MTCI7j` z@ef5-F<*@!-3_N+m-G_e!glvEW@H6)ZVcF_vCOL(F+{g9?~Se;fp@?d9qc+F`P61XJ9v<#-0;_i_U3d9n+z>V=#?>9UjsEG%y)M& zo$XivAvkd8CuL$#+hCpvvz9ShKZIAj?sxGAZ~aTGA3F*FxN!OezVuO#!gs#lc{qLc z40LC$2!+IrOeqGha&=FR>pbXCrss(N&9?_XU@!RGX%*?l+Jn2U&#!ZRXnkeUTdT1L}M(4@x5 zVg=I7$l2P{^KOROymQflZN(ce_vM?1*HkVts(6tZAL-n%~6Ta_dTc)BtO;4f>-wpB!*Co zi0j=5*7Oph*^E_PS=ft_e`X?>D`B#}j=#C}R-D<`!WVwQ{c!m35p18i8;dGvM9_{$ z=*cL%vre%^gq6##!f*f4ALBKD^v75^d=x2-uyOh%9(DgOz_&m9YtZg)AHTe!76kMIn7}z;)JLiHf~fjC2^`slwdya$F6F8gW=2$zn3y7O{%uRbMbg> z(&8Kr)DjEIB?!d42=bDETvTN^g|$za8`p(|lGYTh846H?c09)N;xZTwI?34Cnd0Qx zb2xqa4s330VQXi*ba+N>!phPT4jnj%BS#KnZS5dNO+wdCv9q;>EE#?7pz|4+TQsRb zLu@lEa_1+%27$%`Sk53$Xh#ze37VGB&vt>nvpY!=wxDKnp*=IQeY2u7K*r0sANkoi(`_r_Oj{x#e4Chf*9XIE-11#ojTi6d;*tc)x83% z$ByBBpZFAB{>tCLh3O1eUwH+-_UT`aFTVfhV|My9y7|nI-RafJUZ~H6! z+UtG~iw6#&Xo|So9(GD&1Hof%lF?`-1D(QJ-AZ5Ud=F*8! zRcP`N^x5zngg1w4R8$4+WD%2ggsgn!OC>?!vJvod}}gRF|CZLqYq>H#EI@torm1c9tqjn#P)28lo(qXSia(N{Mq|HgkSp2-@#~k730YQHqV{GeJ($a z@A<~(<3PjMJ#!XIlLgGW44HL6*MZX5ny-#K*yeN4m9%g;lnf9&zp>%?jH@8Y2M!OW zX|a&A5Pg>A0K6oM>E?CpVBjjjV1G;)!CGu&=@*YvkB4Yl$ghaIM~N*B}D-J z%nfoDw4)KmlLeeQw~Y_|&3p02Kl~%SwB*|Dr5CxyZ)8{Q5we(ZP7GKSXwxU)GnIqQMTEQ zb@rsEu_AlsAQqFG0UACuxTv?&(@KenLUGDO?pq`Q$bEwmUxAh{fV4FLF{`EZWE(nM zswvw9B9V+nEuE#1W~YR%i_vq9)w{X#iB?s##qQrlK1z*KgUyZeICt(mx~|8V84K&{ zn9rv8rB}ZOXU|>0*FN>jk$D?C+uJyF_0{;mXKu#JU-3%hW`yfBigCE7a-u*tj{O5lK&wA!F@paF6HXibz2OzZ<5IT^YwfJ0M zP=e)R5tnUPbYZI6TIr=j(*K@BYJXn;O@Od8u*LH7Wf+eSVdFx^4IjP>H-F~iIDd8v zxy$H!>z8Ne`ij_TrC~)b(F6-VI~URsId@it9YRo>jX=(qhr1pP04#eGMkhc!UdGA0 zHZbA^uqKFR^gCPF%7R=}hxPjqGGsQxm=aVox|}iHo?~^ihZLsbpheuG{!fVh19_bt^SMYB8I2P$V!nSU+?GpS|ra{O%im9e@3iQv!CKF^Rx;|st39`)4XYe1}ICEhO zFa3Xh5wCvr>+y{*{3d+qlb-C_8`q>TO3SXKD_NO-h~r)ZD6`HgJzeehNm0qNfWNSgPOxD;uzRc+hpn^!4M0U zDl-pGZizv>ImkxtwA2d$)M8<@>?f38$l7iXZ&GAHWCx z`dRpv7d{_X96yY$jWg&4XxW{6{G{MSKnLX9A(3FDg1ozf`(Jq+SG?fs@%lggbNunU z{u(=*Jr-8iFl}1A`CaeE>D?)w_7z`=SH13axc%HY96Wjyv*|WQB6!i)KOYag=3dym z^Rrkcpfx?EX$wL$RW>p?o!B}C8POr|v)&n>DKS#pqVbfNeWV|GNkSZ)2rjKndlo+S zj$<-(s#CZNr-K0XPZ>&2U;pe=)nkHG_&fM&8pyWIbAw086_t!>*Mph{tE&f*mGEb8 zdk0?s`ZwavyH8^Mz#)tm7cre@>~?~AKZgPsFD_wqc@b;t2e7ujj-|C_j7AfT+mUHg z`i!odV>X{+XL}19n;STP_B^&PY+<^+gXzvL5(tYc>sVe{!?}%fc=1pEG+zDM-^1DS z=h2QvKKRIO@4~{q@aWxfO9ff8Tj5FJm#SV|!2!bnp~6Vb0US7ZJ>K)qPvRB7{QJ25 zmQz?-m|!wm!t(N(a|41s?8YKwvTBO4XhE2XsZv7^!>D<^Ha5=z#|a8RR_6*TQU$iW zTu~xqvC=O~4^ZQ+y5adSAODGeho?O8@i>3( zZu16+?1dT9_W(kdp%|$(+;&f$z+`C!FZk-G;ks+?jsN`G*W&Ior!ihz#rm-$_|PYB z!VNFK8NCV)965|`x{KMy1$^%}y#P;o#KUm<=1*e1O&F^}`wqlxHFQ|#=kSk=kXzlk zC0w*PA$Qqi<_c_Y??5N&mi=|G@1FY}XQA8kmGvB^w5UY03R+SLC>r#rC{rAppqMDF z!=I;WUGLMS<~wsBB}fJ)iz~R-_4mO?KXwy-;g|k1KJYg;U}13?$BtitPJ#2=JD7Dn zR@c^W-F4UC_?4IA=&|EiKd_E=G%|lqH@az?7NkJ$U4@hwk_EkFpcia!Y~k+HcjNZY z-ikYKy&b1doy253!qVzG#!GYDeEY3v(g^K%j4o#+VyEU0MiZ3b%hggp%DfoLM0D}t z5DfxI;w3CET#Y|^fPVaW&<`9f*Ri>YB)leB!$67mt#6|BrX2h{_kNJl3^2(xEY~o zMi@;NaBz76D@&K-hL3y{&-o|M#fyLZKj4Kgd;xa1FJRu!F-oZ_jZ-WPV{neh+K&Bf z8`Hace9?VB4@bY_+wtpfcmqE0iBDp(wvNT6RrEPyp=prk9r~?peA{!LgC{@g5!kr> zvsg)lF)5OIX(mR~W(BCrPARSfYwe_RYBIZpn{x%aZjQr;4&nj#`vT;0ird(`M{LAdv|*Wl3ML*S-CpELTdM;1jd zy>+9AFrCkBf|`G(uCLh`r-bE02XN@}qxgdR-w)@{oyTWBbrWv*$cJ(2uDdZBHCQ`v z5VKidoUMWx8j7E+S`M<%uaJb0%dnX;75$VRiW`yz}j!z%RZ0wHVVPjvqUM z+2#%|oIQynE35dz>pu_IUU?-BtS*B^&@`#)Y(V9t4$J|y83uwlA%%-s2uRA{7l4tW z5C$3z_MVu}qSxDSu&tLK3ImhGoti}qoV1!eyE@ZMWail>Wo+%taLaAC=kw+4ub*mX??BoM-=IY@9z=xE0(qfc74Ltc#O-LCb8Bbw0!HsS~)* z;Y0YrZ~7+u+UtK8fBxPNAg!!mal8PL9=jVC@SLYT4bOh+m*d>6pTfACVxeh}!jJ~! z>c}7$#XYSH5=f>@bu9?8AcMi2(DgGMSYN}Thd&gZ<`N(Pu6AN>urti8bWlfAaG2FS z8BoEw_`PU#DypLtfFc8@u({XB1|uM`nX^)hd9P^33plqm!w>!Ie~aJvt>3}Q+5sH8 z>@rN}bL{L)p-gzlqaKcjKJt-RTU$dX8PnMuGMk}oTePDVDJ5rjv2!~zRC+XQu3cSk zfw#{+y1vJJKErfALn6Y;`Whbb=ttxJ5BNg7_ucQsd*A&onnaAED1*s9m0iEXF<~$zUuK$ zz?VPfF}U*J0gNr8Sl#O$h0J80+d)%?LNzq>=Juq;(KHp#E(ccNH`Q8lI>01iEWOna zG3!9ctldre4HIN}7X_uaAD&3|Av#e24L18JKK|L;@R~RMF+On9r?7DF5H7#$7;e4e zcKoX!{?~Z$gCB(Juel2I-3^SJ2HMS>jach(J3UP9yvPYH6x*jyV{v5_FM95CapmP# z;8lO{hv=p=%%{6}=9fJMU;p&4z{agNW75sB(zf=xZio^aE4eU0+}_TDr_qtAI8{k8 z#WBHJ26DF9k5Io?CTAuX&!F-hl8pr{1}Q&uMTli5S=23 zSGH`Mkr%))o#PJ%TU$iZ93U zE3ZJ#z{buNhzO&0gvs(UvSiF=Gn_kn4rk7t#m3eac6WBsO?LqRZIiIDxQK=_al`V*S+tJH^1pkxN!Cy78V!Ka07*4KHasp zI>Eg^9eA}`*IB#ukP1Vai>}1TlCZFN0Dt<%zrkI%ZQ{s*gXng4G0K9kd)Bk?)lYmp zM!Qq!)&(?O56yk)O{6_zd#t43QnAKr3xTitY3ZgXI>=IERJ&G{U`%l)=TC_9>pALU zw+j;=h*M`{qe!|V*fe5zMmuisCD&es2fXMz@XFtRBYy9#ZwJ6Qe(W;bcH6D^g`fLH zy!2;(5`Aioe}I}&s*8uhG&jJiq`*>Yu)DpDt`|J>agV|EmtT(G{N3Nh{qOrIe8X3L zCGzPLXm+-+kO1z*OR$XWslK`~i)KaNi_gq(ETw>kb@S3dn%<kZo@^?F9=8)u-5C7F$wAcn|P@gGb)Q+&avVucL4&mO{-Wyk5bv4%4*U@!z z%x80KZ|`D07F>PZwRrZ|Jr95UrZ?f%TRw}W#if#c%%qKIKW6pEwEK*~LOqG|-K1;391!4Vo;3NgpBZI z1hV$%c026s>|%Lo9pCX!z6NK`Z{W{A_+cy^y$p+!CA{TN{uFoIaRSGW9>i>S3oSQ5 ztKd;nPJAtxbqsf9uqq}H=#VyPiC3TWp+h8QF^ z0bB@%?NH->7tS+?wy)E}$<>-+fi4JFWIxS3#m+*Gve&`4g5F=7A>pJYF}@I$LhV?G zx>FPZ$mraC%ZO6;kzxi%N%e~0ON@F11<>`1W;DjRjU9aF_kKU#_E+!16<6I0N(mcV zJLqzU$3N+b_~I}567(QkxNrf@c#OrxF?M%$@WGF~AD{U6O}P8iDa_|Hq|{(M9%E&B z*%Y%brjkCRNez1KAvvR)ci6aa0jKUhiJNY`2?r0ZEB=`*MpEK79m?Z(44W-G$6^&J1`>?^PKRurK<&d%~)*P>hug}9uh%Vkiv^I zx-DSzBF@t=VH35jqw67wj#g)IM8Xh-ihEOR<}isJx<|#JUvh?}xh+*N58l;?Kuj(B z;9rIyVcZZpYVm#l^84|YzxYdBd9Q2G_k!K|F1o(M6Q2AeeDP!d5zb%O#C+OeVR;Fn zf*Wu67~b=q_u$OwQ)p9*@nnqEl@&An%^ha*Ip*_SpOun}`xPa|s7V-&TP!UuL3IVQ z=?q&Nn|Sv---VBT_@j8x!ybxxwlP^;z%%~wGx3Jky$-j0^3#|s zFIg7WXY6k8V7#~pPR*W94SXncQ4W_mSQpRLByO;Ib`!gsGmL2&vz;AWy}pjiR@R`K zTbO7@qi)m^tjI7Gbz)tNrboB*{g~Vu9swu9K7SJ<8lG(TyaTr^$YaA5S++)-vG^*%#H_NKT@%FdllQ-Uklo*Q(OBjtu==&ZU=PzJ3 zor0OMxUz^tM-E|eaS`LTfkEM9{2An4F&QmlX?Yn7?UK!;oX+s}x4sQG zf9lhC>=Pc3tFO5i&Y!=4?X7JrE-vCLpZSmRyRUu??!4tzEUvCV=Na?dBc%rIgdB5= zHV>BraIeJR;<*|?(an2EpOHoh^SNMUvV`T)7|qTGQtrWM+}1Bzm5iK2o)c=Hcu6w6&9ft<3I0Ddci!5ESG8Y#U zEw(Ls0#+9n>=f{T)^%M`gogj>p(`%O$&cTNooHE=}BGXlD5Wf0osmXaU>1JJ{`}c2!+ioo$>w za~5~pb~|qP*bVs52R@A3Z@(Q(32UosSQt+*o{VtE?YHCiUi*4H;Ym-#{qBE%Z13)3 zYiAql>j&_(ul_2$>Nox?Ha9k~I9WvQdQ5k>KuNJMS+LkZK1YM3+?$IJqV%Rj*JtFc z$eq>wqtrnAj3hSQH_v+j8L4TJ1(>M-?HD{B`9T^eZ+Jh}lRx|mExT>}Bn^*5CnY)U zI_TM;7>rvU+T=jQ-!u&}g-Y2RaGdkYVI z=tJ-&k9`a-Y;0jR@36YQj+;MoGv4w?Z^rKS4i2mzK$8+SE}X|~X9o{_=tJ@B=YB07 z_>c!-eSIC&Ht2dm?m9D+RYA^#)rBz*9X*8WuD=!!f8@jPjAwo|PM$b{5550`_=7jT z2_Ja>2Qh9&SXx@d!Sw^!+1$Y&{{A0eb7Kn+fAph}r;4-Z&f@T)L-_KiKOL`s)$gF| zI<#$zF87#CchNQpZIfI#6wKFQaJaa#hz0<1Z%3sHA-fNw_NKa{-j$KckPLJx=-V;2 z6}bJgCu|d_$$gmy)3)v3O1-AVHK5MY3@w0pL9$tOX|VLT*n_FoL9fhvfuL%>T1958 zg5^nr%MPxC=QBvxA(62yDA~qp)D`1x1xc-Z5T}VNno&ZY339`@>Ol{}Y`TNHZ~iP+ zL722HIM>;gD7Zj?r-9K3h!U9HqmeAz(PVxUGs)P@9hxIYaoxio1}sc4mmbtMF}n&- zFBJ@}G<=2`ooh9i6Go#J3kwS-WD3l<_)CVv2^BIE!U5Z_FyEam6 z;w^8%Y%U4LKQGB345wpz?WSrTkzX|@=iYc)mxzGBu#`7#+@WT2MMULs*gZq!9gg`p^3!P z6DiZI#6d~Hv=nxePB~+n@Rn9I%Fe{>O4a;bEAP)SscIJYtT35XyypWSNTx5=F`Z5@&hfnG zKcBh9dDhm~>GcOpCsVHf@JAVM4_RE82O`Ig9_GnUxt5>#xu4;Tv(Mz%i4#mVk5d)} z#wM9RcsfOi>z25};fjPo)pf;iGN$&CSv_M~P1)Glq}vh1y?IQpPvz5&URy!t1ClD$ z&QwV8W=26q@so%#$jPfS_H=0?Qo6DQb5qC(-&K7kU1_ubL#G&Uczulk8Y1SYDfigj3LD4a{Qai`_G*vpZWl0}&w%E_#zobPewldj>w<1U~Y4~bPJk$>`j zgE|q##^W&R=vG=T>!eLe>PR;?M|W|NSaule<{&)bKV@}gi3`p> z7gHv8q!2Yt0!_UTcW~?25 zn3uo$mHf=l{tQ*Dx&M*-Da(>>ucWR#WrX>;K68Tw24x559JWY`(7K-CqsLpr+WI=L3)oQpL6A+_G!w?-`5{bqoWvo)LB{AU(V0`+&^I&W~^_l)9IF^ zw1bL4ugl89Jj)AOqf-(M9VhGp{wXRwkNyHe1{+WksrWW z%Ugfzcj$CF^txTPMmv1sW7qTQ*T05^#d&sicR1^ubGiJ=E4lfrUt=)8s8Qqc4@sME zd2@cNU4;$`L1fh-m7*pay}DGgn&cMbCsXz>E%T(yF5|;r`Z9hpqIAh|$|+bVa^r}> z84JqnlLyvHHlneHXnbR0Drr|L|8Au#=FZ+XF|Uo03u>F5UdzvBoOSRZ&wb`I=vvEc zR?#V4GBH6JikgBYmyG_@H98oR+Kf7lAzk61aQHf;O+&mM6IHoUoMs`EGuhp_G0U~m zUd7yY1tA}mWg*N!F(WYz@hI&NQqkyrls(3^1aC$Ra+JT2-cX9WlV~zwq)H_f#CJ0U zp}Tuwq6!eKs!iEgW0J+@iYSD%p^JePUWj8~_~MuO@>gzTZfOM{BBRleYoGcQR#sQo z*xJH4%XBi~^Pm45CIkk9E{6|4!nIGlil6qI$Y?eK13&a5KftjQ$N9j!-pBIF zUKZyUc;Mc9x%K8-c;dBBVltd!o#Dw(c`A3_aVN9cjIxvbJA&lG>KPbwyY;Pg6-F~I z#U?##{xfbM8>aV^MS-uTbg;bsMbG2ftFLBzI%5{W7EUrK(i2n^Dc7)csozfVbm_z_ zTp{B(W#%;qy>q4HJo-FqZl_==rPXUPlQ=bpEZzuMJS)9E2L}VH(Kbb4azAgKm9*$2 z8^@;EwoV?J{yVAo61O4}qa)ZvYO-ROV#^}g*)<}DlcVd^vq9c>a%*;pWok^8bgMBg zTCGHlqnFSh0aSC?jd21J2Dv{jIV)K!qhB(nV_MvYhB+e4KWmzhA-TuOnAAnDT$0!d z*^Ssde%3eVy^6{l*M0b-3@20OR!eq=Lzefga>3&-WICJR!;HDXB6r?#C&v#TrC)Z~ z+1_N|-c^3$r~Uz0*xcQw=ys_^XRv;29-psAH-o^v>Jiy$-BE4>(uYBoCTy)Xn=?w;K z@9c2sj6L(=C)`Cm#k*I3r3EjNghCgfbqp^qQ0v5wQ~d5=6fjlJ8@LFdH%N zN-o~J!0xPKQVF1_+yrvOQdki=yYF>E5iCV6@a8CLV1v{6_TW06cz9Rph{n}cRP$dtmRcaF9B^ztUP zQUs`lk70ETDHT)Wai0gx>3xdDevnNn##F84WGdzYfixSU&Vy*(>C8wYQi z9FT9l0)oP5a4M?&OU;7PrGs#9m7i}TbPSm3i1EndCo`;<{;ZDgD_V} z`L3mFluQF*r5Ot@32lr`B601Fu`=lpRb48%tuQ*J?2RE|cp-U7*jO{L#YvM6CmTvF zMl-5pRDjkREAKI##Ksy-)G17QkwO5zfCRk?J5&W{YQCb0R_dS?buOr~VaO-EZLu2_ zFwWT;8L!b@QA~4WRH}N7qAX+*mADT^R1_x42+|l~SPCX%isZQ=;9GWNwrLSLc@_%S zY!c0VNOW^`rdI66m0dpnv-hhM_4<4oYFaV zx3)O<+_QMmOJ2nB%@c|IRT!$tgkGoQ(7t`l_vi3MhtX)n&dvy9ErWia;b_d^<44&V zZW9f-P6sQ=Y5I9IQ-nNeuz4enCa}4)#WSD#Y@YfTALUaY`#1+qJDqOX<(6A+;mT{S zrYJg$CR5Hl_Z$u$Jiw7h9-%+z5-auD2$^R7Em%M#3?isGtAw8z<>%+ool1idkHSDv zbZ~x(@wH?sN*xS17b$F7U{(^Atf_H2@)P$+VkNkF%(m#3Aae0!8f%S_ya&M~vXMz> za1EOjkOU}f5`a4lE`;1JS(TLMjZjQ%4b4W81vkp-WOJDiD?Ag1@BWp*&MruF95ZJfp0E>fps_B%%IeOwxxjh`S zxx3BA)+WQz7}IeW=cq!RK6g}q5@~y*1PUSPWX@qDSQGG}rtEci{tKVaXFvTJCX)$+ zUY`@k*Esy}Bbl;z%0MQ`6)R}o2(LB>OLa_WhU zJ2^e2(>$6;2sXu_M}s$xS!+Q-L+ z+kptq6wJ>fT;(PRi&Mdpw6XbS8Xy{yJn~dy9eZ*Dc9OU!N2s8-|3dA?xD*>FKxd~n z;KBPJRb0L%kGJszL%b z=U4ah`7eAaO^2){`pELqo6rm$I%PT@(G?&`$9l?_SH@rnA@|@$ zFbe&WeN18_4Xx2iL318VQ*Ebzs7cL9KxNF}XqLmr&_cUo26l}Ke?-1+BzGcl7lK6Y z+9Qk_k|JG_Ew%UoTmI*Y{d80|+`dW^P zh;vWgxO^Us9gM=|$hR+KTf%4(q3Y0~ zLtJsiW!!z&-C&?omOS*pgM?WHAu^dv*t>5ptE;OVd*l%YoenY936GK7(S||%t#hRo z!z9m;Xj&;$F=7K!iit@q5!30iTN~c?r|;#CBS-KONgn~@w6L}mg6>+Y{jU>%QZ@%G zMjzH?#$@t7No|D{i#KCj+DME9*FV*S0~3L5#83DrHx!Zwwg?M}k**u!e;_ODQ5Dq(f%y#5&{MF@t^&su|~9Z~^^( zpN$h+7?+rkWoa4o`UxbKObTlqvueub#);&0;gU{98JwyEI!P5Fso_?5IAkZRWGz6l z9Hpu}zOK09vP*g2yWUHv(lfKYwZ-=K4rRAsGMzBj>$7*?J`O+d0H)g!Swh0lvR39> zh-OVnjnPs%8$d}}8|8})bX=mcPNspP)8&!F5A)ft+{m=sp>hSuvZQ&4^QNPsA`Kvk z4XBg4hL{$Vj14g~Y|9qYPl3%w+R-o!F;YP|C?kC7%U`8u46l9h3z{&OY`Z2(P?HEx z3W6puR+F9O=ULZ`1T#*NxFJrO@PH~vO+JCOH${tDS;X0y%8Grux*yTzQ$=Ynnl{Gg z>5A$qMCmH;Ed~$4Q`b`%C)`KVu$;9?t6_9#tx{#hGh`GSwRKQ6tf(~QbwZ2;V>2%U z8vjg-qn@RJeQ4)N*FY|K?ggOd4H?q66L+$JIz8&m8c#sf%64iA&1Ycvz zf=*ec1t^h;MQPdD*=BufBh|gmqB%9m*CI58Tlp>{5}u?PA2H>Zy68~1V?&y%O{Wu1 zKkEzzbA4)GQ8>$FGGS+DlYMgs@Z)4uG`}#Per5<6v`^1Vi*o%fv`G}YjIFmUc>f_m1o#{ps1y?YVaj`f*&*J<% zb&Qlnm%6UgLNJEpOC$A*wMllKCll%ST#K=36+*F1bfIly4|%$mNLIR0_xoLjqao9} zBD!8~B|BvYFIMs~Ms{{~F)>olDvC~*g9rCh`#RsO5F>?iSm)Bu4-r>5#+ecZqxZUvV|<;s2VeFba}cvcH3s$ax*Jr7tio zmb#vD#-RgTap~jvHw_4%81Uy zkT)l(nzIW&rTbG_rkddja1r@Ble=EgUJGR8PM~xFx23+DO`;{ z46Vwmp%*l2&fHJu%B&SIq#K>mt_hMzCOQyl6Cy7AX?UZoRAsn|JiV$|-MhlGpZzSR zbtRjgl`(5*C1yKevA4#2G8r;?%xaS%uY5OFzBeoFRZtJgM@?`hRn6YLhj{St394B| zG3euCpy+le%0~YZf@d-wr41)|%Ccl>c~Q;~F$A76Cr3Kvyx+CGA1d7Z;(`UmdSKVuh-{0U-=52c-2+Rgen|k z>dn*0R7fhK6xHw)d=Qh3^yPWklB`g?YXQWfo(2M!TQww=hZ;rwZ7IFBPgxa{5&IXH zc=l7C3bR=vb`^!O!Q)t2}2Elmu>Gdy3l}BktPQ; zle;pZrcqC3R%qLyN&Pj%C`KpvGJHAN4a&qE2sAf8ps(*iQFYm9tWAM0L5WjxGU<1( z<2tgz51DFBelB7*)lq=27G~&VR#`IZ> zB3bQO@ykpTk`y`My=QxOCoydchp%ff&{bD5ttgOjye=jyV|0@bDVH6*oqRrnF-7v` zFpi?=ogTAvTufjD(lqGME-hPLv!D~PtS9j_=$;+(CULEox78bIsQhk zLf@#46LqYX7PX8PoudvOQ@9jpI~%jw8}Q6aFBNbzjT%GBqZdkWGGk5Vrpa*CNk5(m zCXyv!b)-h~FHtxbI(iJzOG!(vR-$!MKBmRwVxVKd4|nmi34QBwDpH8z6s>e{o7kdt zkdj?a`yyvYB%MKsK35jXm_u|GF{@L_-6yIO$*xST%{S&Qp)rI_f{xS*WHVejA+umg zqW65A$Dvwk7bbZ!`dOXQvZWmfx#DU4{|L1aig4?B&Ml5~w;{ z^_~1vyin2FF5`N)5guwFhB4z2QW5AVV zqNUeq!?a>@QCie=IHuyj5CyFhZEbIRH23oJntN;=~1fL?q4!Y zdaSP^hCtzxUt$ztPaOlXEU3mv6_Cod+SfQ!P$1+doAMk?q1kIlf|i1Rscg#>;X2)P zjNYhJZP>gZ(x}wyyd5iMwORsu3`Kg47{OFhJcvrt&%^{Kjag$YX;cKFahV0^F+^A3 zou!hD(P)xX+!l>1RupxkYn9+=(v;ZstAR@hq4CI+QrEQN=d4ka9Kk^}Wm_i{PD-T+Rq}9m z#Aq~1n^=Uhu=IL8DqmqlfLP6@5_DvVu>#lPi~Rpww28^-ibeX}Gx^X5ZsqNN^bsDs zZ->%ZdS#iU2{v%=-4AlZC-3Di-hTrxf6Y^P*(>8@sPHq$bCa!`Q=+E_VuB4uM zT&Kg@)+WQcV!qd7vOA^MDIwGX9(fsI*$g`7@jy)W@-ZaZbqEn$l13Xng+eWgXiW&0 z$I+1K(q&OlYs_2_MWuFm!GlR4oqi`V1gH~{CPC7vbairygqUdC(SRvRs@hOF%Y?{@ z?QNnfnAJ7bIu;feWT%%Kq-oenbW&BQRS>P|&-G;1YC!(OBUh_?TqIw&f$Km5DV`swI;}Nm{ueU@h6;ME;&mX_U;?4h_y?FOS0! zofzgu@qk$}?ev+=DyEYut}H~+>sVM=5Y=uPHQsmMeKJWA z(_B|11Ld+q1Sa)Nn3*mC%92l#UC3lS>jkKEc_Z+Rq&<6aD)4G@`(~`gR~4O3hqYrz z84ZW>c_M?kIp*gVQX5nhI9D(lPjU-rWYf}#)L(UJb?Aeq(^(?={ru;D_dc%sz|HjB z9H;MDNfVLEGo8$c*06VR87%B?7M7`aC=^9f*%i*&X^S>{yHGKo+uE^_+=5AndUHC(UH%m)SwbDVzWY4|wJ67XOUPDePo_X}z9 zSs5kTXj=cZ7;hwVlzdXfkU+U)%pyuiF`kXa5&}dV!B_%@+DGxTbTSo6fTkV@(pWB$ElB-Co7#8t}IHG5X~ zu(X^a@(Qu{+u7Mlo107bYVY2?xK83YxL%(-Zoh-gjVi@`&zvw+UVBDu0leu^fJX?5kLJub_FJMXxI$#{YjUB|-GBK`R} zysuN`=RI3n+rsZrtV%|`Hpp1|f4n)Qv3noO0eb}!B^W42W zaNnJ*EG#l93*Ps!k8tJVFJ%AfJ~r2m60D(Xj7Vvfypw>U7(IGA(=%U{rJ%Q8FWgk@2TUx0Q#W0wvQ}vnf~<0+=FU2z3B&ExXei_N-9% z7kKAie3+ftjK!rrY^<$u#S^aJ&}j#l&8Bk7+B}i5BK*;lFGQdFd}m#AM@?(N60^iS zwH_7|jKsI4ilB~Ze3))k@~WuoNW>*BNgZ&7CB`~)1VRkNy2eTrfH5}VM6sneKr;69 zJ7`h9bwc40^`=rxQ$HodHghs&k}dGuN&bu8dkW{MW;IL8tDJGh8GPmIcV$W?1;?tkPXflG3%w`G6M-!tJn^aTzGZ>sRIpr)y zFV#B5Dg21LwxuHxAFdWeU<&odnj<6IDDMdL5J6U`*V28HJ5Q< z-#&JR6TW=&t-SlsKgxrL*I3%Oht-8eKJw>xaKVLV@~ZE+f}QF<+}{94!iG4*7Gs|J z%qy`4>`sRCmY2Bop@;a7fABVrR5gP$&tf{az?QQNi;`hkFf0moonyBs*exAHS1`0L z{d!R{vX-GS>{`RFF$|4k+gNsO!O&TT)-Z97QQ;UD1-sTTGL{jRF@Y(8X$({$&7ZRd zAf`nZWM1!$WoD8{Wpk`z3s&A-inX2Ka|`hdk!U~P#prGyN* zv}mw6Tcpj$q-w%@a7CB-rB&t^S8$y^<7tg43anV`rxLf26U2&Nhqz9PJ2jcdBK{dB z3GJdxj=VTTC5E@6!r-G%d_2#rX;o2ndQ7LD4}bV$SYzpw-SpWcVo^a>YbBPzG?ehdILYBe+|ko! zQdpNZ;h-#6oy|yFBDQvHBf=Q68num#SQ?C$Px zVr`A0QzXj2;8oU1h*Es?g4Gc^bh>kFt_R-!C)YFA=~0@3osBIXciLWln9SC=>Y8)7=+Xn+cGFS%D}9RPB|de_Ev*0Mf92ILcs^I0cP{gbdl-|1L+Znz z4XJH86`3TX%O00HR8o}7*i@Dqmrgk!lKF@Wk&XfCiqY;a9q;LPI%zKKGmkd?Ea&h9 zrom$dixl(o%%UZ@EKCMLx4nTXT70L&{hM3-$vfW5hd%oSip3=;x@@ekaq(pr^Rkz| zl&TuxqH-R9;IhNCvEXZ;dta-knO6YMNRWz*2!({EjLGEBkm3cM%yt6->nz3=)V0SG z8T9(R>rdaq@%0n@tv9}bg~cVNvlJw2TON{8cnQI)08K}zQKK>ipOgg=Mb4dpa51t8 zh(J#{Hnl@kTzbjlDT@+=p)5~ZeftlxySvNE{=MuyuwV2Zp0ad2aL*&`?(8tv>!pfC z?s3Dr_#aOgf{%2%%Y5>qcX8ywT~_D!FxnlmSQ`Gpk9;@JxcqVK9K9b?O(Z!yGTl7J zQTiuH=fzr8$}asn%-M@M2N;SwQ)RIT8sDo-?y<>f;UodAW?GH8^0LcVSzc!B1HJwnTRU4k{LsT(b?`LCqbZ~Dm~+oR zkFS08E9`7+F*m=+@wLOe^H1K%&;H^+WjGpA*LAiC@IJLtI!UrRA&Vh9aYeBM$a)BE zOT~!UA!fj#NsqTCd6mu&7TDR^<lR`RjM#EEKNGXuQGmo_00YUjBG?j^2l@hI9?MA{_vu zS;gj&2e|sui+JLdkLNEw{Wa!#tJF2zcH4bC@5Ps3Y)OpuW6Fyt5HiVldX!7AKA*qy zqc7*z|DX48^u!4kmj=uoSjCTLJTxBhz~)vWHQAO-p!MJwp~hOQRCrPF0;R_429myH zpYtT-gfa#@34HO+yZHV$yoRS=av8&;M=0yaoV8dR#CSHKYX{)Ex@6p|}k5E{4wznCLNBrzR{U^NoRj+1ucOyAQ1raM@F*f-c zIt(Qi7b0OaWI7t=UOfblFn)=#5_DPme^M*b_)scyuB7Y_7|ai-gQxa^`8|92(pSF7 zKm4hm##+aL{fBtl@4cOUt9$u(|Mu6|oleDHBIH&k3FMQNZ#2!QH0lPBN45>sI47OS z_vVREH!}FlNst8hkO@L2V;(@xKQT6qFYCJEoO90PipwwM`pmSHjy zmst5c#R;v5!9;8;geRez6;Zc}^&i&YJy_FWX?~I4`K{mO?z`?`@5(-Qc6Zske;*fJ zd=a&uVg&cu-P-29d+wv(8)V|!NfdR{m=3kC5F)PV;;WJq$9K|U={+tQuDS9usK>;r zVo;V)PYHDb*1J}GMnlaNmtDe#KJ&HA$lco5#n+k*`q*9?ON_OF;pib=@NJK0FyG<# z-}*rwx__HVF{0mDpff+9yQs1Bp~VG|TOuMYXB4H6nRcDYMl^?97PiQk(unzL86$U{ zSmRfI^R0Z}YhKMupYaUpt!=i4+sqdwg}7}eL6L)^VB`aFZh_9i3f6U~jm1YR84p5L zjp;Os&N7@#*je9ZVPTG+`-eZvkN^E2Wi}h(eNCrZK#2HQB|V6BglS3?ch!s|x8A`c zcif4aO%vZrc=~GH7x0O<5Uo&?o61512eFBVQO+%J@WKmNI^%Q}_8s89d+y^cfA=js zc>lwkb>=x7Upva4B@KNyrO+`=Tc@m~+#9?Oq$oNuD<<^&z zD9j=}07^l%zA{#@9!>U=3@y<>cqv*6p`_PU&En!7p7V^S@zLu)O#ux0bID@v=n?jx zeu(kJP*pP?f5oMI{kB`!II+RP+#JK<4*%uX{|k@5rB&6zglJgtz^RV@UG5kK>I4+jq&;;T2_#2@_r+ZYT6$s1xe~Mw;EKw)4IgWs(up7CbZ?giI>6Kzb_4ienPHfOKya22JcTQuO4x|AmN2X6bPAq% z_0##$AN?U-{^FOgy}M2fv*ck>i`i?D{(hK6IvC7!%3U{nj^lUU#e5wYn1rGyG(6G3 zZ4{Si8g-}A$4wrWf;R?=lI^WszIguwoO{hR%$>H)kNo|g;MSXO=d?p-a^l1Z#-lO6 z@~i(h-|>bwva@?M6Qi2o;1s=AlqI$(vv*|78?yp*Eva_eDN@<3JPHj(6tBxh`by?a z5KMzR$d?_&ThyB5^O0}}$BSR|0{-)F{tjC^J1p#3VQ09@%{Se|i_bojq9~cnD(2_s zdGgbq%KP8_UZ&NI)xG<;_n!Ot*T4F&`M-blmof8;Y;SMkdT9Z9d5DyQ5as7$zyzxe zg-Co`xCK)xTjfp0PN;#P;?!r=NKm*IaWAyTc)6(Md4} zRmE+$e4S3Gn?~iq({nBX(3%9F$?Yd!+PZan9R?DOXl;Q9jvQa39L!OzTV}qdTlVm# z!j=WKH&hr3SF*9SCDMarrB@C*2|N0qQjh-iuA=!@CHu_S-MOFnG2M_$0oUo+z2 z2TriLF=9Hd3H20NP;r<(MoQsYby!?l;;B!2D(`;(b@Ub%n4g>Hj@$3# z(kDE^!PC!Rb7za;c*MCEp2y`^U(Q#)^i^D0vS)Q4pZfSG`RBjzi~RJ@{xo~{?qhvp z9UH`W*9ps&kky``Ua3mx7x2|up;Jp+Wh`~=fr5Q|SJ_J2-IQ0K?$~ z>nzWC-g7CsUB=T9RW)U0ahb1w?Iw;sbc9ZSfcG^;ku3FmJS z9^i|gy*DMQbq9R%b6?~&FM9!V3#-g_P7p&yQFfX7gqaodOH5+P=f83jw&-LX$=PQg z0y`#B|4nWX3QVPkV>&y*wAyB{Fvn$2n5QTXVkpGt(8+@uTQ1IeVzyJLLqmW-=kccD zq)V?KqruT9#yKv2!Ug>Pe}5-ynp& zY=JGhsl3MGt)T)2iwkt-=CH;Q>YCA!F~)nGaYW;|?9$8VcRH+Z9Hw-Uj*B8PG8h+A z=^Sco@D#xK*f9>@atjOI)2#xou2XsEgAn^9kcSlMXM|y>u`@B*bSKy8!ce+`&B+9} zxB_zv{N{iBPkiZXw{ZGtr?auU%f|K=|Lhlko}c*1f57g}8dWu=*Aqcgv|t^fPOVF) zQ*!LsaX$BjFVgLGvDgIcWt>26y(&!=NN#yd8ubWO>9TC-jZ>Y;^zVctNh#kV8H?D1 z$<%Y`taEtobDqhEKK4mw<1xLtc}82CeDMok;H8HSB{QBFsj9$JpY{xn9683?k)zBH z7Fb^1&voy=j=HM(iGTQ$EG@6FyStOvmO`md(T*)ojYzI+vgAx80);Dy9%fZV$Ca$E z?B%|D?&tshrC;XjH{HtK)dNf>Q>N1i&w2jyIs3c|*xcTvu4{^-WHcJ_#V>r3;8UA2 zt!tL%7BI1978?CU^1l#nN$XP3x~jLi{K^Y?-+MlX$1}II%vW!}hu?ntd-!|b`?qo3 zlJUkdY>WhF@UF}9X=n4U>psnwzkVmZ`334YV{yrG<&_uU>)n<)(PPJlC{F&L%@0XE zTunXOW;))49M;mfE32p9$+eYI`pCd#E`(a2#c2PY_-*)LDT^gu_U%{kxC>6>KmFU^ z=Z;&Ba$seZ)iVxq*YV^0!hiUWeE(}-&8wdCTqY;h8EtN}r_;x{QoJXUj-wI{NEMRX zbxbowYM+#ulkrH1P8FS^gAX%&5KTw+iKsC#;H+cg$PpNhm~##{nIs!K=j0?y8){+F z#uU)S6p=! zYbVwTA&LU#gsKhxvhj;f zg#zR0I#*otIL^KBAot(7&g$ZR7WN+C_y6?I7>y=;*K1zR{*^N^!Bdn46C8i^fsgVV zzx_uPbMtftT^>1ljOTybrJQ%+K1P$f|3>6m#b5nfEr7>0r^Ww}l=1)dPXCI@PjiVk zpfeEHSYO}a+>6fS=l{ju;Wz*DyZHD=ZenR}p835iOm;^6`?vi8_a8aJ_rCgd%ncUV z@dj^-B*wCdi5Q~I$=sn%q^d&0YeL90U~9mKy78LyA-_e<0l|Ww46(7sRsj<#3X?*H zoX#9Wq5-=oaRDZ#%nXx8veqJ6fr6QbSy?i_cOQT7j`#B6Pk)BRLx-5f$kD@(@I!C@ zVSe#n{1W5Q7PX&J7Dj?`s}!W0Jskp0N?U6T4ogua--js1xmjYaVy|}Jo;T`N@NA1S zp$1k-8QSO#)ZwV1T0422)*D{Km%n-=)A5i_cb;C^fddB) zu)6;MTicsV#uHXnR(aXWUd{*K_W?FHH&|F$VE^7jJb3Rz{PaKkXM*-tqQ#@TpIHnof7X-qlsc zlL@<{U7qm7Yk1xZUdU)NPPmizEG;f@-`)4{#m|49Zc(N&cc|(0d(6!b*xEp14o$XM z{HvFMp_=WoyeIOyzx{3e>OXrcvwF&4ZjpX8yz{zG@|iE*#Fdv^#ObFUWHOy{>z#M; z)mv`ImtE$T=GfWYVr74qSG?xwxN?^`nzopmkMWrEuliUgUyl`Bst|L8l|e_)w+{mCcsA+ofzLM$wQ^q~)N_{0`J_?_R)dFPzN%zQoB-epiO3ca;K z?K6=H(I%(~O-NGZ1f!%yMC%e4ucizTlKK&V<^ZaM=^rt_SbIv`7#g#qO(PN4Dx=!Df zv@X^bggSEc$PqsNr4RFo>p#U8zVJn6lPQZ!%M`9)I2<7hrf(L$gdlSR19|?<~h%K0(ac} zJl_5G>oK-Ne{mVt>2Yj0^w-fJL3C(ql5w-pkUN zXK?*ZH*?3o{8fJXAO8&9y{ptla1o`}s$fZoO+oF~0@S`qT2cyq6weFO&O1OHu6#&> ztfHWf6}69){dww;qUAbPiBp5qI2{>H789T<)B$S>s;c6^{{1}VDNn%^HcP!F z_F6uLZVpWgWn#mcRFF|8P(fN56PpS{F$~S-NC0$b?C?a;Odm8aeIJ9tfVsIj z27`GHKXRDg{>|UwUGIDsXPk997d-B9oO9M$>_4!dxxoPE9JASswY4=KeDEReyz@@( zx$`d8j-P;FSzK6Raej&EY{ur+7M*UF7rgKVTz&1eZ13(QI=YW^Iwb~p|9jrU`qASo z&dm|38KL%KeYeHdLN9Hj>bar!;IEuHCcwI`cG%fD%n$zXcT(3K{`4K!(b-vKVR03= zu!s4@6^!@vr4p)UBaR$DiYX?1_xHV==fC)A>hZ8&7Ik5h;4 z5OAbvL$Z^rlh2?y94iq=uMmVEKoSiZ9JNmx2)Z5CMJ zY~y38$c^M;B5qA{^YhHl%_YD$q!Fl5v|rt~Wtu`xDWg>-AZccDAc>pT$yKAx6>Xa! zL>`eQa$LxmfTa#lmh=4TFZ}|4>rLOwk&WXl?b*k|V2-=)xRVcGcOB1u?(>lFN1-*xue@R#%*N-Z?zu+0Ww88E3Myn|{qUcT)r=4R*`IUQ zZMU*GKTix5(FeLkfyXkPOga4UBhpSLkyi@taye+JIZGRYnQ;~q9plLm6Sw%OfBfTI zbnz2-*SoIc;Ro086*yO>*D{viXH>HZXP$RD-~GMc&Wm316pkLhmEmY220K;D(DrW( zr2Joh$mfSs~^uV{qm3TZ-4C%`P8Ss&fb;N8SLH5`p%dmH{M7wSV&Gx zK1d>Y%+)}QLHG&s6VzrtYRsLOjfCb0n_|J~nbilbkaY^|T*`~&-W%Xhzt z)sAJdu}jYw@ReuUiVovg_Xzp#SLsoMt;%{^8fV3m| z9$5Vdm!c?m`m>+OS?8Y37r*pH?!Wt9Mx!C!Zilic=?oT`8>H{pi_fV# zi@Cy4h>KPrGOa3hc18q0WB;MkxccgAxa{&v0odByqV_dKr(~f&M>QGq{&&BdyS{!4 zbBig}tDaWa7_pszDY_J8Nj08g3yX8<9(kXl3=#b4tFX$4ji20{s)gy7IPffcL}v$ zXE-`cRqtY?)p)F|=>E6cApX*2z*4bx;tm#;FW{g2{G0jp|L~{$`3G)bFu#v-VG(A5 zN*A|Ua>XD9Da|M@ieZ8}ZRg1VNv~i`ka1;NJYEc1Lr4KTrrT#+2mb71ALpL6HQery zyZ`%J`L5T#nrB^o6{EG|_-cYB8MRh1GMdgj+yzc>i?Kl4$BMgJ(MYh)0IJB_9 zPrdnvIcsr_>DqA?N=K|`iO6J1>JZX&#py!LLiUWYs3~sZ16XF$DXu88>XDQFsjs0{ z_Ue$8Zgiy8%7Yq%N~`ANCA?VmJ9#*rNIeqJn>a23=Pc0&I)!Ct_XMwa!SngmfAP=x z@t^u>qOlbHIhL1}xZ~Da*xcIS1uuRPD|`2`Gu&l1o3UBf95{4{Z+q#>IP%cL+~ZzxcEFarB`_SXx}h+CV)W)Aco_u}pThu-!hzpr5L@kQllX?;&>SlylS} zVN|&?GvI?`ap?@Y{(f1-~L0Ke&$&$?paQY zc{*WddmA4ky-tTgw+A8e?)A$#}%l!a{QPo>nj#^PHzWmFGY6>0JNukMY?r zeVO6*l-}ZUsu(;tQ?R=|bmt1lV>ucM@IL6pWh$V|lXR))<Sp5 zYHw>(1Y0(1(Ijx$yOPhQgg|S3BUvULm&kQKT7sYOkKXd5^m=pr!=L#%I#tEo!ZJ&9 zbL{MF^Wi`HAotySFHgAoY7Xq%PZc7=$p~LhvCh%yb$Q(5AJ0V>Kb{yP)6s<4bVf~z zUo5Pl(<$h7IuvC|z%ZH4sHRgUA>~{2x;=Vjip{(CuDkgB4WHq``yQaz?XfVoAi}gj zJ)Lsi>4$jLi(bIN#W^l|-D`N-<(KmN?|K*a9X~>EVGq$c2E7408z=aG{?&iyM}G9x zT>YfW*;v1uSv_L!>Ny;JIPfd~;y1bNrX%cMIl#*5fasx~)zl#n>M15x5DN){DrBPM z8CZ)e`iaW#!1WfuMyA!6FMsA9?zr_nzU%v5$V*>-HN)`(RMYL&H0qmk{cpcP=#@x9 zk)n)XYC@u5SpxF%vppn0h}FUm<)F~ zZSQ`5?EAi#-}s|H;%oQb$HLyd%#7i;-tkVhha+D8!WU31Eb-9pF8|@JZ{@z@$C+PQ zW_NpsPVM>eANm2Vz36f5Ja|8Q$}+hZc)ZG|j7K7|>nV z%LDg3!ms>`|HhB~_?vk8GcTvE$DBAi<5zz1KXBI_Yn*-HEc`4mKCwmL8s>|FvT$@e z9b8O0mS|GEW9^gaudga5(`niaeT6p$)9KLb_c-go*=+2t^Be!}1B`}a-te8zWGmi{ zuXeww?8;MSgUC+G!6(U^^v%<{rtB`zUs@tWgR>>3C}i%h8H_$Je&t3O868H`2D#M2 zLB@r0e}m4sLttPnqhm)obN>PU-uJwT-}sY1=1aHV#b9|4aekgZ{Id_TSp{D4(wFn! z-|;TKde=QH?OUau)tFhu_k73edDfMeGd}hZ^D)wiF}XNJmkfPTI9NeZ792i&gd4x|RXW{n>ygk=t%yvb@H~{FET%q+&_vCzG%nw74GYThy4cXJ z(_agX;?u@#T_-v^q0&nd7$&uZ!GI~b>~ZJv3qSXd`LF-&x48M1+vrW^m|Iw4&*B2Z z$(WnI{1xuL{dUec=S(2Rf&P3C@`hT(T&8&Y&zrc z(Ieb{-@QC=_dTo~J5KbT#rZkv+Oxg6LBBtjB$hG`?(_#7-`M1j{`6gZ&#Pa?*?X24 zZLV`@agiVU&Np)I!RvYd$FFC&yG3_li3592XJ>qZfBi4t%53~Yyz-SV%u<+p1f(v=>ldt8>{rf0wq+=WwCnPlh4mPducJ-N(x6UgqZK zD9a9=P8Z`WW#RDEjL~eG^f#js8=IS~9Y4XbBS+X+KS7wyD6q`+dYIr@KYom*iy4n8 zsIfuPzHq`r>7=X~C9=7sFT$NmeeKnpn2dSn2S3aQKmBQ}UtrI|3dc5Te&@IToXf9# z3-e2R8IO*B(=t6zkqtsVY)DDZ0iRQq5MobNha^81W?M`N8jYe9Ju!nUisXV6g6JL< zw3Fk{8b^pC{myRDf#7Sp&S43Rw$|yzJ^b+NU&~<7=YyZVfzHwrZqVoW<_5jFIf$O| z#wKrk#VdH#bDz!lkq7Dfnw~R6KS?74({MmEb=nFhVToc;f{bdlW$@w_?esS#JEJw$ z6neE0QIR($Ie$CN)VwK3vLz)3_mC-m%?zdC~>Mo*}{ ze6B#RFzjxuF&*#n#@D@yr(Sz4Z~v2b@`+D>hDqg_Us$5(c37C}V~t}rnsCppcW@`S zQ8|YMp0#!U{crys-}%yS<5^cffywp;(~S+TzTkXL z|KY#GAH4VdeEh~6aZ5|=T{^^Ydxt-J??+kg&(jM9WXWpZbX((2A(kAIDqzv|f>Jbe#4 z!y}US^?yK%agt~(R_Wx7sr0rQUnhxRh-YAmju$0j3)$6#DxDVS!jmRlOhzOFqD9w6 zB*o4qd;SmuWl=C+IEFjhRMRQn|JqlxxVXT(uKO^|FR;C}$#b6cWZwM7H&Cw~#SV8_ zC`w$c2s2H_isE-!rEE&^Hq2qPl8>l947yO&g5C$u>b@18@PsQU%1+iywD^-bRZ1<| z61_a+aO9Kjk1l3w8+D>hxtT%qC06`44Gk?_2|_8-wc(a%;Bu{#A#H!M)I4n}RUL3e zkIOH=h?l+Wg*@=UL;S^uKf-k%xt^PExsBt;4?|sJ$}ZhOpH8<&rzo*yiAVe|e4wst zX2Tt((}T`JSH6@>FS`V1Eu+zxozakfufQ6peMPU+p_a*) zaTXs8Q)B4NEi#!+_`UajfO{W$fWP&!mvY8|)7akH#tr8A_Lsbz&wu?k9#~svZh48G zE2&3g1`%HSq8GBKTQWU(i@9j9)hvn95Q81fco5csO$jriZBPZO7d)VG zbJCN0=4DKGg|^QGZK_R{ng5$z&y(9K5VDySl3J^)45lcLtaK*qiyGAN)RUz5Px;|D`YS z)vtV&yY9G?`yYIW^{p*-##3TllTx8mYt!wOEH5r{;Ox`5@PhMr!j+fvq^qvs$}28s z@5&N&J!UlCVp7$~hJSI9Zl_4$(k|gQzMipXc@KvU9^%1=9$-8iVGBpunWMW>a>Lhe z=kSr^eA~01%~h9O&dTyCcinXldrUYx~Si@+2or}&mlgrLOpHF}N7P{Rfy0+xjo9<*fc_M}D z5aak8UT>$|29X$qAsNehjZYJ9Lk?6GGLaH_kl*uldW9H8z?AIgjf$F#qVUPc#FtfUjz-@gvbR4Ob zltGMzjVx*-w)q$;VWQe-39%Bogm{rA*_PO?`IE9jn$$0OeeW|6EMG6Bv(%Fv#=|Yj z-hd}xbqP#=V^bR@N$Jsw?;lORNt2c(fLX>3 z(rqoAAnPV(yGtWp3>qAa=tYc8xge3$Rw(}9)o3?1I*_6$nj~{9rbv^H(nbnvsjDG7 z>qCNp!WJyecUf6Ehb!C#I5`+X8q`SkYntr%P&1qEus&WF&>6a=sA2_|NqIMhZrMrZ z#35C1olcLft!?hS^G;^dihfyAk4M<)l-=Q!ey_`5afwh@eC^)5x$(9;aAm>L@*)_h zcef~~Bfj&sZ{(bV`x$Pm(eL+}#2W7dgQCFrDm~O9YKdaopT&NXS{H0VZ3{*|GV%%N zog2*2D|(D3Q_eZ-Y{c2_^#td?)3e;MgS_)^dgFdx2%m{R?!OOyIzDx)) z0jE7|O10kF!a5jOa6Ym4Qhoy&_>11BUn>gg5E+LW*Xw{wKwPvor(Pw?HJ2#Tf~(k| zV4}^|?vS_v5UDeQ9aGMxME(VtR77=!7I7MR>SLSi43qA8rQL*ST*?U{zc<7u5+vkE zHepXwaYb`amG;Eg2EM4))8VdGpeU56ArBAOA7+bj+Jx_ZrSxU1hqpL9cMw zkfI!oEfURJG%^88umz#p=N<2RA78xnb~^n&3xhr{d-038@Nti)I}6MW<~cqY;kuS; zHe)=T;);2`p`AglQ(}WqBsPJ16~sh6jiy=9+1Ej;0c#Uxpvl~Y{N$80vLo6+UciP(?E{@oSAPrQcjdgV(A!yV$R z!tPn(Q=htlPu=)+R+r}38qX5yj;o_^V#_jCTD+}x-;EIyTv}Q7@aHdPkDfxf;Iwdv;y|S879MNnmO<9d{^Pf!U zTp9)nWm;Q^Od!z-h2~W@^VC#j*n(LI>^bKw_FZrR$8Np_*Xz?Ol2Hr6%LbtyR$j)3 z&BCgKB#}$mA5FwKN3Cz6-)C$LM@G9Gy7F?C4xL6-*Qs)`iBg?O3+=F*+GC8REIX{N zt#SPL@nq|tm6jpjmze$I8mz%6|G_kiE9+icE>uQz9oE=nt%qWpjbBD`xr$jUqV(#h z;xJ2|Uc``2_5kcZu#bKFR+&{Z32PSqg&}8rBzA0T3KLbKYlL8gv~GIp#{7M#Uu0v! zmqy_+q=}df(&m*eiQlS3=XW@K^ps^GfTCfvv(0#WgMBN@y!2U5=c$)n#OJ>Jd2YPr z7LKp2Q%!1mWx;d5?FGE#S!HJZ@ROgWx3VXdPBOMGtf3IALnl>| zwqrU5uDvH(M=>{#TUfv@EMg}Ue5Yhm%_z!3!ks0JJwi3Da!%_v%BDP}Hi$gawQ9GZ zfvm~$&4)DZjM7&tn@k@%Yh_dy4b~J`TV!BT=mnY59fb4~j3Jbcv#z>|@o2)~2Oea} zT1so_xDtR+SJ}ss=o4QeZ5;`%47f*<`iX;Z@Il4ljD@wLJLnBTT0g_Ac+?%!3CgLu9SC?P*M2Hf_M*{tnWvMDn)WB6~#U_!EqES!BCP^yo7inX$)?%DvI-Szrzslv$ zdlq+m`Ae+d|1fbp#tQ#GIJLl+(H%~HmXx$Ax{`+!Yly}XoS~eb=g`?_a{kptF@ zC#AQgkz_#21D9jJ>WUB(m>aEyB8@x~87&1C6;{&bjisuguIe0(`Hfzu z+y)V2NTp$v{?hA+G73CzGf7B^NJ>vmW43@56==-95G^Gq5oZd#Z(5q*EaOnq+q1yM z&w3`)%`NKPAz?O6A|a$*kq3&lZX%n*lFeJJfRC=*p3NP@h{qOlY-9Hs5Zu9?QCO-adfIlCj>@dArAN}_k-o?bNabYo?F zTxU9Y(e2~9eLRNgWCS>R-34|=p11wkbsRo=jOShZB+lHom%elK3X8Q7AK>urkdJ-* z{k-QR*E1@*xNeWptfnkGDSbExfeT42q)Oe*EGa22dgrK4&;tc2w_LnaxKy)u8krd8LWhuqI*BpZdCMh*qG z5{RS>^&s;4D83WvKI({B30rw$WK$~m-n}c-zCJ0FBxQU^dnfnk(P2%^sIuW|B=)Hi zGfk$ct6859>a8bLMGSc(v4s=vMo7(J3OlFl_ZV2mZZu4$LxOQEF7Ic$yTJ!O^98>2 z_1n1gymL7F;6YZF7KtHn_~=o-e9JA|cl0=&eF6QZ7;?Br&h^bN*2taDBFg!IW6 zoz?5K#HGIrM3=z9G|8}dXet}=$TUJCilQ1dM*S>xos`Idh zx~>TVwmDa%jVFZ6M?vtQ3><`V0;pq{cG! zo>@qTl(XsCODtgD;_k@a6*ciJC9#>9NMqSpxuufzF^$TdOJXi9*rHF9v*!E%#@5~` zu|eo#;A|4)Sanra<5j87qe+tkrSv*$AV?cw1)!U@G9qNs5MxNZhF~%^wT&bfPDgnO z2vas~&5#&j86ROhBtuCy46A{ZmJH%5DXc_6(0J^3s~UN?vhUNUQq_46A=mm!q+{LPJNQrr{`9(@2&p@q@rA zNlf-5YxAjRt;L3zECU5|NgfkL_MAi~EP1NY=sj#Csd}xlU2QYIjk;+HPGkj0HU=LX zGoh$c0ogEpNR?w*SmqX&um(ok8~B*SfSukPMYls?)1gurC}k6_Y+yLrWi*-6>-N+A zl{UcHBDIh-n|LG$U&9iNw09xkg%vObn8~RYgdl#Zuq$o7hD9C2#@WeDZP(qk; zDr7|IRB|nthcaD*1STVlPu=|VzL*wpsidiB@~v;GGSdCQi?A#-1h7;R$+LuE<~@Zm z3>KFtO3P?xlgW5U9Rl6LQiUnibV%VMp_($CO{sj%8E2fv%U=CTKJ~F1SUx9kWo zp1|>l!4{U_>qaNjVx5xlvE=&^QfP)?Ps#0E1B-O`&|yvdCR7icavOv&^@1|~G@wQp zl2Hf}#_GsY=XFi+Y`U3N7OKpijcuOi+(V@~e%Tc){bmr>-u zabALa95<~>6JRg)at#GsS(7M`r&MDhAz3S&197HOtwf;lk+5ird8BZuht(c8FTfyx zqB0vr@RmW>7Lm#t^_!F&b5X=5iGA}Cqx@l$!*>iW%^*|TP!!TuXk{3aX-jD3dT2YC z6^~J9*%qrnZo`Vo?S|s{de1|9Lk|+2C;STauShLGjR{`HrYU~FU?_S6<}K`0V?q^~ z`5C2|u(YpBuM7B$pzPDn!Y05r+X+b3C?djW z6V?!n!B{7td8)pGI)_UsRum;ML?N3&U5EcOrx@giF#WssW6IjIDV8}?u9KdkwMiHU zxEARqX75F9I}$rH5Tcf-fnbtvq%{qZDrUZb?)c3<6s=Y%LYHl;>C|UZ-;+!SP``!r zX4B%)gshlAHfLq>V97l<9Ygb+Zx1XEX)G#PUndd<_{# zY2QxoK}?!aAB0OGdrrb*l!q>Okyc?Q0hB_X&b$>3Lp0XpjUc9-S?;A&T`I6M1w~#1 zCbu}5T21qI5QE>K$!%5=*l50!RLKOvL?DJF_(~gX%Kn(ekW$>S_}Wttna;+n9_aH6 zKmYewTn;!pWq!U(uh$1UjJA$pO-=1>SVnc4xHuVSCxV$1HfVb0CdLpe z&&&r2hKiC68?yI@4>i%I5TUwGx}e6B^c%ZQxeY=G2T}z#3$c|ssedx6hk`-y1@T)@ zG+Q17UG!v2{FuQib;Q>8!?b|ArnhZWcp(Tam}a`tkbqkKpi+{NybEJmEeN?hMye6p zDj}6RtvFO{y+0-XXx^rk@oOFix3m;dC+8A(pB7e-sXX$n{~27D1Gf`dk>i(Y%(}V-bfO#5t@(| z+oB^!C1cs<=gJq9h<>h_7SL=eKIdB|igGe6X^kR{CP?cFOe7zd&0!VjOt?m2z)P#) zjDnF#=B{{E1QB+{>?4Cvt##q5NiA8H-MCp9Qu*Jmij4ezP&s!<0I6yDW6~<~6R*PN z5u|BNJ~FW^2QedNf~f`3acE{;isN_wu4wr=lEh*N7-=Ol#0#tSB=1bcl@NbY#WEBF z5`WT^tuD`riTE%j#K5%Ppzz13{TkLK9!sa}5d2V*$^tcxnJsx}cSLV{$aHH+MeD_?U!dm;_Win^txI7GTOXP6A!t(1rxmN$S12ljpTnCHCM?$SvTyE%X-}i zKqoBC0zk2a);FjXNrOVmqEc>sqS!U9h)JQaaukYpq)F1_Z0ai#P{^O9Ir)Pe^17%( z9+#P{cGW>jmV@T5k<8jvGonjuI z5Ua$CF^*XsiPo_@tN1T}{3mp+OI217=476!$s{cyBlu{jqG4(x^yjHDc;gsWo|!Ay zu68*w+@5vKv%0#%w!m=T^ryruJH#001q6Nkl<+sgOwK{l&26O)rxK^ihBm^TAi2q8B3Mzevceub>yY);tNv@@|8 zxt&x=WDY^=wN(R>N1!UCld)fG9NMT(h0lVQM#whzHpqm^G#E?DlW5uvS_COXm*JH) zDq<~RW(XdVTF-<-NClJgTU)J_axKIxDEc6b)sUwNItoVH8Wj&3r~!Hd)@NRTRq)<` zQwC`gqT%Hbl!;Ox@p?MFlJR&) zN!c54+dcR4?|$RIa_4<_(x0DaT#p(1A#Z%ei&@=Qvc9pID_rwGyE~kU8$|XSkexM% zxrq7)7}o;9l(~ocL8JwwG*^9a@<}1(y`tzTyr$6=lp^1~E$?a3un4}%mP1-plmUPK%rzx6jo@Q7DfW-v2k>J}* zBx@=_m$@l2jum8E-7E5YQoNj0+KHkPG)_*Xw(XpW&3_VaYNKil3QI!#z<1N4#IRA(wZ(eqe68<){q+kxTK%XJ+^Eh#33Z4fkZ1`-FZK~@_Ot__;n#vsVr+>&HG zBSK*;bwtc08jv)|fs&^lq|GezO+u@R(34%-n$Z3bG;30swbohcx~B4;$6azU=Uw!8 zwnk$SiDf955KN;qHdfrtEQh9Na$@ZmTgQ%2_PcZjJqEo2A&hzW=)GKg`RV+A{E*XKYT)iYo9jWYQF3g_e|@Hq%@6gvP|o ztq7_{j18C+XmkTjxh=1kOK)9^2CPHz`zVCy+;(K*QP2;I7Wa|HE5T&9`ZR^eq@T>> zl%#EuXqsr!XjdmKQAfm5;aDq$M3W|N4XrV*oPKItlX=<5#7A;9O!BBuECu-STo=c4w^JdtVb)%bIR-ZOh_q;d80gc0g)X>=*vVk9ZL7V z?U0U8#?$KvuJ!0=QlkiBCVS*>EgI$8h-*|);EH5RIG)Ye8IEu+QOS) zHzqLG>(S|#*uoHuC&r51>4e?wHJl-MBjfk}Xz z&2-s9$u?^Al0{fCpJ2MFG^L<05@o5Lbo-o39*5GKw#7_|B&EvAbTt97I>$~jc*FIu zqOdgCKCiW@SeG~fdY^<7kY>ngXK%7>+?s+wib6E1%P{)AX&P5!%NLc@5<+hEbaJ7% z5#sNmzwEW;lMK@|_LbaFBYvdn8>+;u7!!wpKxnlpc@vb$RuC^xV^d04EK(KYL!{I( zm}y!LWq)d=iOxM@MP!;r$j)Xvf}}SQma0oE$~vm$5#srPfZ;*1Iz2uFDnBEd4&IOP zew!D*^hvz=@4kZW+!`C34^XIkf4)=S*mX*55bX(}aUFau)@=eF8Ceh*m#x_&idLJG zDi1K!BD?l=q%Im%-OH@o*u=Fksw<#D`pGgV4dw~WLnWpkQB6B^YLNqUV^h{jbrbY` zWwDoj(9}{8ZN?^2u_SZh=3|ZB!~t=nXLj8rqQF%ZKIc}K^?a8b1Fob^h-6-PjIrp zgF1;zC7G1G={xL}W!B@kA~3Bg>N?<*&tPo^RI!oG-4nd_HP7WIe&RdW9p1v$)=|lFcdoerz0eK+Ej(fX;kP?E`hN*r0@+S{$i# z&L@T??O|%Wgv{eSmkH8{ro%^{CM{xJZ=%e+jrKW~AUP1(vR_W})V4SQ8J)BY0@6_s z^>}I3>k6FeCZd~$6$NJy;+Ub4iOYwOYC<0-hh|1J?~kU%$N*>47NileKt8I4Atdt` z_2QI>KdezI2gbN&6lv4OZpDczeV<80r{%uMkee`1Z8ChQ@foB^dbN;2zYGBCp%D|) zRTZ;7Mtm?BkxVyVZpb)}jWkmk`Fv_xlP522!CLz}Q8Xi@%{D}nH|Z!~vn!H)!Dwel zHJcG5bUG!SZjYI*6OCPgOPlVe7h>JseREWCjnU*N*82S2eIrwgM_;GN24PP62hL{w zyyi~E5QwhGV?vCK>-2P$Aex!?Olwc!9LswTQqll<8NACN)yeN5O+QrLtjxQR%gUf+ zCffo+TLv)g)fvTgDnz08h9<)!4>qC_wITuEn3|f9sl!4Q%~gj^RCLj62^>w+Q03EE zO87w1bl5gX(7YS@pEgf})MZ*3q!=20hj@M_gC6;-t%-w%gEl;P_5OC5zS;C8lo1x2XjPs)uhR_4?T* z5GX1u?@=eWH7T>iiblv9D~DD3{lXT^W;4G0r7yE{Vx0iYFU;|zYoA1CE}6|ZN2Y=M z81Z$DF;MvlmEXd+>YHM5|3A7;kqshU>S354088?u6iRgx91UVSEjZ|Jtc7|Kqc z-~PR~am#JDG98X^B(y7?n6eQVwP_=d@eo0gJ{FlN8BL24Ors)fPZNUTAYNM6=79|{ zq)~0_*R85QTZ<*)<#XFilu}h^1`YF+$27VO8Mh`?XELX^VoMjLm(K6Ebpo}ff7)|M zB@rp*-&2pTj8v)B)R#jET?(&ACWnzM>n5KgG}MO8~ZhYo4UmGTGHSV4}lyJ8bRja$+*Nk!~ zw6Lge;&n=G5HX)L31qgC%ZdEESI{Y+B5GFasD-ay3^ttSP?@u_mckUtO~~5j(P;rsX})e5Ebd`0 zDR2^<6+ywR7?7yD6#B4hBd+A5WFePPYom)_)si8X$4Z}8@X^WQRjRa6KusTNqp8nw z()pNe-?If=%WE>2m=|g~6%${;%S6H{MsiZN1#-_!OL{F_2*n+wJxb#`_3CWm?vznz z#3Qp^UdQ~N9__A`t~8OcxeALSLThVKLJax-8{ggpnou3xAks!EcK4E*p%siKjOka> z=q?0n5KJLR_)ppGQ^jOxY^`lFIgBBgI+Mq;?VWN?|3~XHq38Me(rH_i(BfzsVqFN4 zI)s$>5qx?!ZNe+8b7GZf^XHF1(1^pwZhttnHVA`eN;KAJQKZ$eMfOREv?%Xnq1D;l zS*g2X;1^x?9neA`QxPEBv?(&3{w+x9oIDi0$>4GV6lq+XGtVJ3FPd3*;=|x1?J4Sl z4?3?m+14L2QKE%okdKy-otGL9j2L}-c#X;CEXtwKQKKR*I(9VTxE%ni<#t}%u~k)& zKPOVMr`|e&X-()^YqJzVmMSe2Sj?}T-)iGVrGrD0%V(*!WO@|kyT}NKZ{-*Sse+`{ zNbQ}?3Q)DhOW{90xk$$pq!XS-gOHK6iU(!XR0p3YPmk4gN^KCJCAs2jlI2LCaE_V!Dp(a{rT%bfWfc}Y zwh*3uM93yG(h?p(2DMsaN0SEa8H`KMt&GN6{k}?rwX$$h&KlFobU?5W{dE%?OTkP| zN}7eJe2A#y%#f!uv=*pdnRF!8jtbB6=(EKY$kUX3FGWz4-KTV;kx7uV4QSh95axgC z-&LYicKWh;is7OmI8lWLBPPBE<6|BTH&}|#315vvwrScAC04b(I`SyIVZ!Iq-?XX* ztssI4O}46#utN5#u$Z!-HX#`hX*U!jCWH*;n#6rmPE`wtZeL=Nm;vJE&jvB+2~OX< zW}nL6jIqVhknhDdZDVMKL_Jp5DYZc~!=>iSHcD8s+J2>zYj0n0zDEgyt__LGYK5t2C^`xenG$@mchy$!BV8toT{u zeX6X~oM+<0Nv+#sah*~dL>_+$TBawemxW>E=C*s~C_C}AsxoVB=0nTsUdNnzDd?oc zmll%VsuXlvkH(&qw~@CI;nX7!XtZJvAAQr4htT*App%v;RZbgJ)V-#Opi{|vPWp}r z?>y#YD}C^?79w~eHD%$ju1GKMgt})FQ#FJd=N!|Z?n)-%1kn>{a@F{y0O zG%j4a@B?j&y$ttfd2nZJJS^nKf=q|ZJRIw+N&WRvzfWS@{! z1Zen>4?auIBLRwp2kAT{Xp8G)e-dhTdvl0DFiG2DbJdsrnaEPoVu*!7G~j)msNjMx z#2B$IN&anSvWSL9!jd1$>y+9c8tgdgv(+rJ+(MY=pB4)t1W5%oNxRt zmE04JdL)P_EO>g(QBOu}ZJc0tdmCTXIaebB>nGNjUtDH&?|zD2muU#O;>sgi6qnJB z&5CiYrijbE6~sPyRA_Fb63sMULwhn|#EPaxV-+(nEIN&t@00p^B)YSSH1z6L#i0u) zV-qw0uQB8eTEXLl&7|pqPAwE~(&rp^TJr9DOVn%TWDREsoQH-37sRa;RLRENLg17hy&F0Yf9va(n>dp#DmrKc1 z6**xtn!G@CylYw;Q|?=Jq#08##dtVkklxYx2ciwBi(6EhBCovJi#w!(`T!B|9UT-TgfTVqyLDMLY8 zAs>=cmG_aN?9eSc40m@qar7vqbreOJZo4AsQN5ay@r?qoPRwAlN2n}HYm5B7X%;Cl zq-X&#hE;Gw$9{=e*1~zzwJDm$k3qllm}tu}n*#a9L0l_0HG&m9N2T~CiWNXK+0+{#@Yj0tLU#Fy^Z=HmA?X>+15lOvl4^Kx|K{6a>BLIafk30 z(z)fDmZHrj$Ob8oD*Q5*(HOD@HC|Ckeef7( zDX?s=Z&6Jql&&C#3S%w3exI_}$5_j3JY_VVP+CV>8g_PeSgdBa?tsQNAu3eXvgd16 zW3e%fvBIuw{!G($S=(Sns>Rg%7DEFp8x`ZpP4pr0EE>ftS=E_&19@KDxN#ZVY^FL* z60UUvaFS@_N)b_$4A$zGsN z0(CMVp3Q8?#In{A|+Szm8)38bolIVp4(}NR}4ECoiDtXn9E5{D$+`t_A4^6_;fj?U)&p`ngrCtp+@4 zb8U!FQAVKZ^N2CRj3mEKzMnQ=Da4`7i_fG1QxNgVikgNi%oXiyRQgua3DE@;3l)mAiMVPm?Q2~4JS(wOGg+oh_> zSs5B}A2Kn<2-P}m2J((YY~@442S|**R2tg|Rh9VqWzoU-NYUvqzp#jP9ehkNZH4R5 z>-CwgKjLe|op zme~BQw)<`^AKShH2A6+^F)jmBR!(o_f*?SpH9@8#Iv%sxOF&c1jdhqJX;#vPQ>3iU zqLWmf@?qNXebJemCt+I)GTmOA%ln`yU^@ATI?V_T0Bl;{rBr)~hHSDupi#iise_bI zqSNu3JwFu4)J+7fTXZvSk=t}cn?I9a0;-=$W;GfOAo!0FZwrw7U#IFM1T+^m_&{MC zMZ4WZ)D;&|+`7>B!us)=pr`(Kh!V3GVu2Tf0 zl&@L9s7}XWWoB$mgjnZ4X=UzA3|Rb05@>b7hF0k&;9!hR4@%IoPu-`~xHb6+Mx*m* zJt|d8p=n(r0~e}u^r2~s5Ymji2Om_p)p#4EiJz3iwoF!c`Jqj^k|1u?p>>3WpeJ3B z6Vl*ZNny+MKAffYk*cb3&PobCJ8A%Q8&dHBVK}Mw4c{UMHg}VaP``yrEBhC`>~bH(1$kLHDPGZX$7bx zH&qE+0!1>jwJDe<1ME8dY^Azkni20~ah(zsLETU67fEs!_v?&50I~z*Nr|5BE{_WzMM#moA%;d)ZH&g^X+7s}9pVbkRqQbC|LuOf1e=>Z)eAGo&yMSEh}p znpKR)V~Lti<49L_@+M|g&=wH+w9lnDE&fImi`TN7CrJBYS`ux6ceS#}%*7~wUN+K< zF_~OhMLD$*^=vlG%^*f--P@K{L9PGuSXyP^k}xhfm4csEwJWv-W`?w5)b$i%pezzU zrQ0oWrIq%#^+Xv6-V>@?TAhY6tq)Ls&Yb#RSYhxd94UBm2vijFnGihKCX44k+POpQHfofV)RWqzH%%&B+ zg?YODKHjIO`iy?c$Tnq$iU(xy$@r!{YEcI#RJm$S(u^c7MZ>^sCw1up&(FF|ws1|W z)hvLRxg_L`7AKh`nEbP)BRpxEk~BCrGaMTSuBLjhnhBD%EGhqVG9Ez+!K0o{S>3xt zziSw7S0^u0d6+7nNNLVFj15xdCP33V+a$dim%g{!i=;c^TtNsvwItc;+exb#A&l}* zd@N|!r$~#DE@KR{*_6{xTVe0&B7QcZuz}#mOh!96lazB-G8T;$C6+#bfn1WB^i(%z zudvlX*Kcgdxe^~A8-LO??u({L&?CbbP5GzXh0uI~k%%`jw*WDp`Z22l6@(<1WuY0#+3Y5&JzZ2vw-*Mhk#<8rv+`5Yd&%vI+(WChX(X9ihnJCT*siS>KDBGs zn3MGzrd7^oT_B@X0Bf-}c)=*a?>hZ25 zmw|~j7hZTK#!T>G){xkAK@$nFP7-+8oT93bCGs)7XP2p$S}i+UWRDVLgL8tzb-IqT z&+KB&?nxNTH~TuJHVA;(bj0dPpGz)2ow}aj2$asSy>XoBXq(a*Opu87%$*n7b!%i4 zW6Vs$3{*x1KNScN+p|3iRt&{3zRQK7NpZ@MW8G!#0eY6kAj~u=s0G>4VIT3S6+D@wcknX zORXwR^0%~K&7fuBiVU`D_2EQYLxg8O>nyH$(lS*wY2i8F z{OgoxF=#D569ePPF<$WeYxvm5Zel#$!I?#BKV|dyQC3dtP!wH!71S$4o@BBOTeON| zt6_=IeWf(c>Pk}2XXL>rmvPN(-UvVzo&+jHoJIQL^vALFNW?jCXgLjYbXYurMsnEikvRKyPl2+KUm1 z=1&KZ4H<>E~HclDtZu7zy zT+M|S>}7ZR?o{%(Fo2YrRp4tVot05_OpCweJ6hO;Em>!A)~1kO8Lh{>iKKvG=PcJg zX_42wdOrsa%sr-?1Enl*YZ%Nquu2SOIgvf$c2VpNP86W=9cbNju4b9k46>56acY=d~XdjnHK77AvjWq zDbP0U%Q0gP3BU`DySTiT6E&{tV60WfW|d{%DJ;mR=mMozE74AxMQ0<0iJUn02sl=Cy^!B=(Ks07ETeF7OB4yu&hX%Rd}rbxjj z)11;dYG365;U`_a#H(I?fHThQODObXIpjQrHi+gj5NgK5BfS1~&*n>Cx{LeoUt@k@ zm44Y}XZ;9uHDmR_Av(50l%O0#D$zo0mCL&DJp@xz=t)V-&!M46WDB<_^KtbyNS=yF zOVQGKcyV)*$UJnTlK@{Qi$4i;ws*wSIkzHD2T+E+xA-fz%{Rp%p74SL(4t4tt5BV|slNE0Nim}~}SOfC3H z;+hl~VV-KFo@oNcVgfc@{?;3cwQj~v$Dbm~vaNmG1Si#B>KTnrUIZA8v@65bs8Afe ztTPnWFdgl3?D$b8qfL6w(=Q{F(I&p$;rqVtJ2>~;dDf2JnGBHC`CBhBFKYWdXuF`5 zk}*&?OYjxWI-FFGg)5TCZg+?)3JTzgi{YwE_Hovk{j_O6#+w7D;s&8R|9G^)^2!`P z{`X(cKl_EZv9q&Ie||4@FpM^jF&%ERymEl~#Ra;ql-vthV8-QBIj84JX&=PL!I_qu z5F+cw%YxN_*dXE_B}Rpln1tDqE|^4hrUB@jl93D(xCTc^Ch?+(jG?J&#P_qw2yOmOA$H@5rYzny ztx1j2hSg9wO8%+Qv?^H`A*gwdFc9T?4{9y1pV_3OL?05Ntg)R77*jMBKqd-Zx;@U6 zT}?M`+q5yr78Z~EKM;hE2R9GmNRxAM%%BVFx6 zjg`vOnH=;~I!h6gpT*L`97i60h>a7+DP0N{w!qfrI*!1e|N>ojbILszRdeS^Xk9JishSlHKVS8Ww3BM z_un_<4}bp#KJ)2Y7>}Sk=wXW@GvqR5p%rP@n7zadl10t903jna!8Z-rr@3u5D-%;0 zX)=&*vTWF>ph;xCCw47h>QriSW9m=NYkQpY@gNCCSj(4euWc6`x{G$Oko4@qf6#|hkc z#yH=?19NntD9$hH@SH+Sl`_+>w4ycWts$76r)fE*yeqRvw42X2 zOJW2OuF7|M_x2Fvz@Vu%G6Cu!8@DqETDzkTO*XnVu<*0G0448JetUmj+a$Ck_mTvx?NayKV<*7!N*iB%zJ;9!cd@;_ zg*4R|jdr2S3~zgoYd6y(kPbe>ix9)>(&72 zdWw8}h)0jMaR2@%==J55i31j0AP^2O0ZdB;k#w-MGC)yu@#^mkFMofH*ZW5(CM8N` zT&`)gE7P+|msU=|1W36=c5XOt{*qIZM(LhhG<_#PIb+SN0HtFSuKSwaDO==wp>2nx zXuQx!nK&bx%bSpRn}hLvuxnyUvmPn5@M-gHV3f(@NExnVCAWeQ%6fs)8ryd9S}lE8 zv}&m)!Y(C14rXvJ_9o`WL{7F73DHWhnl58w<0E{$v5Iax$LZ-mm}Ezg zGK{vU`aLxCPLmZ%w6M1JDGm;R$B&=l`Lo~f=4gUEH+eHw*SffK=QbWbe1PF_346Ow zvAMYfDKCGu?ssM31%hx1L+m=3iEN?O>O;yNMAETg*07z@dQmDX1xc{N6S-#xeivH# zPi2x}SI!&`=0(_d{9)V^N7dX{Y3&WW_;~X$2SHy9Poij6BlzK0wk1PekUF16K|%d& zIz}H)VfPATDyUAwu!>f-Uf=zU3+gh2a2TjR>>Ey}}8-hqhy0nDyxssS^FP<*t za7j6MRBe^=YE?yWgG8cyJ!Y7qm=>7k8H)VO=4rU7FkogotrPTmH_+)1aB?E><}ky! zm_Q~HH_|l>(jF$`eH6tZ%4ufzxmt0+q8cfi@Qzg0byh1B`7sK8?58xHhn=@H+nrJS zZ3Op!_RAttgXRMUpY?N~-p|f?ztOr@mEfPybY3YB><@%1Yq-{WmB|KCtD<|YXt-(w zhnl&(%2ma-Xl@O^moK@{Pp8)2mFBCK%&zus|>j6NLqWVVEc^5DddaVS!*6CJGA#!!S`;AQ*;;!UDlCOqBls^7JHOXyRn3 P00000NkvXXu0mjfmHrKl From ca1381627c96729ca6fa17d401d896fa8582616a Mon Sep 17 00:00:00 2001 From: Adam Scerra Date: Thu, 18 Jun 2026 14:48:07 -0400 Subject: [PATCH 6/8] feat(scripts): add operational hardening from review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - pre-explore.sh: Jira token preflight check — catches 401/403 early with clear error message instead of opaque pipeline crash - create-children.sh: JQ dry-run validation — verifies children array structure (title, type, description) before touching any external API - post-critique.sh: synthesized review history table on max-rounds escalation so humans see what each round debated, plus refine-stalled label - sanitize-artifacts.sh: PII redaction metrics — counts and reports what was scrubbed per category, writes redaction-summary.json Signed-off-by: Adam Scerra Co-authored-by: Cursor --- .../fullsend-repo/scripts/create-children.sh | 41 +++++++++++++++ .../fullsend-repo/scripts/post-critique.sh | 23 ++++++++- .../fullsend-repo/scripts/pre-explore.sh | 14 +++++ .../scripts/sanitize-artifacts.sh | 51 +++++++++++++++++++ 4 files changed, 128 insertions(+), 1 deletion(-) diff --git a/internal/scaffold/fullsend-repo/scripts/create-children.sh b/internal/scaffold/fullsend-repo/scripts/create-children.sh index e49052c15..9f1c3e933 100755 --- a/internal/scaffold/fullsend-repo/scripts/create-children.sh +++ b/internal/scaffold/fullsend-repo/scripts/create-children.sh @@ -51,6 +51,47 @@ if ! jq empty "${RESULT_FILE}" 2>/dev/null; then exit 1 fi +# Dry-run validation: verify children structure before touching external APIs +VALIDATION_ERRORS=$(jq -r ' + if (.children | type) != "array" then "RESULT_FILE has no .children array" + elif (.children | length) == 0 then ".children array is empty — nothing to create" + else + [.children | to_entries[] | + (if (.value.title // "" | length) == 0 then "child[\(.key)]: missing title" else empty end), + (if (.value.type // "" | length) == 0 then "child[\(.key)]: missing type" else empty end), + (if (.value.description // "" | length) == 0 then "child[\(.key)]: missing description" else empty end) + ] | + if length > 0 then join("\n") else empty end + end // empty +' "${RESULT_FILE}" 2>/dev/null) + +if [[ -n "${VALIDATION_ERRORS}" ]]; then + echo "::error::Dry-run validation failed — child issue structure is invalid:" + echo "${VALIDATION_ERRORS}" | while IFS= read -r line; do + echo "::error:: ${line}" + done + echo "::error::Fix the refine agent output and retry. No issues were created." + exit 1 +fi + +# Validate parent_title references resolve within the tree +ORPHAN_REFS=$(jq -r ' + [.children[].title] as $titles | + [.children[] | select(.parent_title != null and .parent_title != "") | + select([.parent_title] | inside($titles) | not) | + "parent_title \"\(.parent_title)\" (in child \"\(.title)\") not found in children list" + ] | if length > 0 then join("\n") else empty end +' "${RESULT_FILE}" 2>/dev/null) + +if [[ -n "${ORPHAN_REFS}" ]]; then + echo "::warning::Some parent_title references don't match any child title (will fall back to root):" + echo "${ORPHAN_REFS}" | while IFS= read -r line; do + echo "::warning:: ${line}" + done +fi + +echo "Dry-run validation passed: $(jq '.children | length' "${RESULT_FILE}") children, structure OK" + USE_GITHUB=false if [[ -n "${GITHUB_ISSUE_NUMBER:-}" && "${GITHUB_ISSUE_NUMBER}" != "" && "${GITHUB_ISSUE_NUMBER}" != "N/A" ]]; then USE_GITHUB=true diff --git a/internal/scaffold/fullsend-repo/scripts/post-critique.sh b/internal/scaffold/fullsend-repo/scripts/post-critique.sh index df3f76530..df0ce1ca4 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-critique.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-critique.sh @@ -145,12 +145,32 @@ elif [[ "${VERDICT}" == "revise" ]]; then PLAN_CHILD_COUNT=$(jq '.children | length' "/tmp/workspace/refine-result.json" 2>/dev/null || echo "0") + # Synthesize review history for human handoff + ROUND_SUMMARY="" + if [[ -f "$CRITIQUE_HISTORY_FILE" ]]; then + ROUND_SUMMARY=$(jq -r ' + "| Round | Score | Verdict | Revisions |\n|-------|-------|---------|-----------|", + (.rounds[] | + "| \(.round) | \(.overall_score)/100 | \(.verdict) | \(.revisions | length) revision(s): \(.revisions | map(.type + ": " + (.target // .description // "—")[0:40]) | join(", ")) |" + ) + ' "$CRITIQUE_HISTORY_FILE" 2>/dev/null || echo "") + fi + + HISTORY_SECTION="" + if [[ -n "$ROUND_SUMMARY" ]]; then + HISTORY_SECTION=" +### Review History + +${ROUND_SUMMARY} +" + fi + ESCALATION_COMMENT="${AGENT_HEADER} **Verdict: ⚠️ Max review rounds reached** (${MAX_REVIEW_ROUNDS} rounds, score: ${OVERALL_SCORE}/100) ${COMMENT} - +${HISTORY_SECTION} --- **Human decision needed.** The critique agent still has concerns after ${MAX_REVIEW_ROUNDS} rounds of review. @@ -163,6 +183,7 @@ The current plan proposes ${PLAN_CHILD_COUNT} child issue(s). Options: if $USE_GITHUB; then add_label "${REPO_FULL_NAME}" "$GITHUB_ISSUE_NUMBER" "refine-needs-human" + add_label "${REPO_FULL_NAME}" "$GITHUB_ISSUE_NUMBER" "refine-stalled" add_label "${REPO_FULL_NAME}" "$GITHUB_ISSUE_NUMBER" "refine-approved" fi diff --git a/internal/scaffold/fullsend-repo/scripts/pre-explore.sh b/internal/scaffold/fullsend-repo/scripts/pre-explore.sh index f943be49c..9a04bcc48 100755 --- a/internal/scaffold/fullsend-repo/scripts/pre-explore.sh +++ b/internal/scaffold/fullsend-repo/scripts/pre-explore.sh @@ -82,6 +82,20 @@ if [[ "${ISSUE_SOURCE}" == "jira" ]]; then JIRA_BASE="https://${JIRA_HOST}/rest/api/3" AUTH=$(printf '%s:%s' "$JIRA_EMAIL" "$JIRA_API_TOKEN" | base64 -w0) + # Preflight: verify Jira token is valid before doing any real work + PREFLIGHT_HTTP=$(curl -sS -o /dev/null -w "%{http_code}" \ + -H "Authorization: Basic $AUTH" \ + "${JIRA_BASE}/myself" 2>/dev/null || echo "000") + + if [[ "$PREFLIGHT_HTTP" == "401" || "$PREFLIGHT_HTTP" == "403" ]]; then + echo "::error::Jira API token is invalid or expired (HTTP ${PREFLIGHT_HTTP})." + echo "::error::Update JIRA_API_TOKEN in your .fullsend config repo secrets." + echo "::error::Pipeline halted — no Jira API calls will be attempted." + exit 1 + elif [[ "$PREFLIGHT_HTTP" == "000" ]]; then + echo "::warning::Could not reach Jira at ${JIRA_HOST} — continuing (may fail later)" + fi + jira_get() { curl -sSf -H "Authorization: Basic $AUTH" \ -H "Accept: application/json" "$1" diff --git a/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts.sh b/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts.sh index 32bf73593..234bce6b3 100755 --- a/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts.sh +++ b/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts.sh @@ -43,6 +43,16 @@ REDACTED_HOST = '[redacted-host]' REDACTED_BODY = '[redacted — private issue content]' REDACTED_URL = '[redacted-internal-url]' +redaction_counts = { + 'emails': 0, + 'hosts': 0, + 'internal_urls': 0, + 'gdocs_urls': 0, + 'issue_descriptions': 0, + 'issue_comments': 0, + 'issue_reporters': 0, +} + def looks_like_issue_context(obj): if not isinstance(obj, dict): return False @@ -51,14 +61,17 @@ def looks_like_issue_context(obj): and obj.get('source') in ('jira', 'github')) def redact_issue_context(obj): + global redaction_counts obj = dict(obj) for field in ('description', 'body'): if field in obj and obj[field]: obj[field] = REDACTED_BODY + redaction_counts['issue_descriptions'] += 1 if 'reporter' in obj: obj['reporter'] = REDACTED_EMAIL + redaction_counts['issue_reporters'] += 1 if 'host' in obj: obj['host'] = REDACTED_HOST @@ -68,9 +81,13 @@ def redact_issue_context(obj): for field in ('description', 'body'): if field in parent: parent[field] = REDACTED_BODY + redaction_counts['issue_descriptions'] += 1 obj['parent'] = parent if 'comments' in obj and isinstance(obj['comments'], list): + for c in obj['comments']: + if isinstance(c, dict): + redaction_counts['issue_comments'] += 1 obj['comments'] = [ {**c, 'body': REDACTED_BODY, 'author': REDACTED_EMAIL} if isinstance(c, dict) else c @@ -84,12 +101,18 @@ def redact_issue_context(obj): li = {**li} if 'description' in li: li['description'] = REDACTED_BODY + redaction_counts['issue_descriptions'] += 1 redacted.append(li) obj['linked_issues'] = redacted return obj def scrub_text(text): + global redaction_counts + redaction_counts['emails'] += len(EMAIL_RE.findall(text)) + redaction_counts['hosts'] += len(ATLASSIAN_HOST_RE.findall(text)) + redaction_counts['internal_urls'] += len(INTERNAL_URL_RE.findall(text)) + redaction_counts['gdocs_urls'] += len(GDOCS_URL_RE.findall(text)) text = EMAIL_RE.sub(REDACTED_EMAIL, text) text = ATLASSIAN_HOST_RE.sub(REDACTED_HOST, text) text = INTERNAL_URL_RE.sub(REDACTED_URL, text) @@ -230,7 +253,35 @@ for root, dirs, files in os.walk(output_dir): print(f'Sanitized {transcript_count} transcript(s), {result_count} result(s)') print(f'Skipped sandbox logs (gateway/sandbox .log files)') + +# Write redaction summary +total = sum(redaction_counts.values()) +summary = { + 'total_redactions': total, + 'categories': {k: v for k, v in redaction_counts.items() if v > 0}, + 'files_processed': {'transcripts': transcript_count, 'results': result_count}, +} + +summary_path = os.path.join(sanitized_dir, 'redaction-summary.json') +with open(summary_path, 'w') as f: + json.dump(summary, f, indent=2) + +if total > 0: + print(f'⚠️ PII redaction summary: {total} redaction(s) applied') + for cat, count in sorted(redaction_counts.items()): + if count > 0: + print(f' - {cat}: {count}') + print(f'Details written to: redaction-summary.json') +else: + print('No PII redactions needed.') PYTHON_SCRIPT +if [[ -f "$SANITIZED_DIR/redaction-summary.json" ]]; then + TOTAL=$(jq -r '.total_redactions' "$SANITIZED_DIR/redaction-summary.json") + if [[ "$TOTAL" -gt 0 ]]; then + echo "::warning::PII sanitization applied ${TOTAL} redaction(s). See redaction-summary.json in artifacts for details." + fi +fi + echo "Sanitize complete. Contents:" find "$SANITIZED_DIR" -type f | sort From d7d7bd969f46cf316213f04ca8d372a4ba0b6eb8 Mon Sep 17 00:00:00 2001 From: Adam Scerra Date: Thu, 18 Jun 2026 14:55:19 -0400 Subject: [PATCH 7/8] refactor: remove dead code and fix doc inaccuracies - Remove sanitize-artifacts.sh + test (dead: JIRA_PROJECT_VISIBILITY safety check blocks the scenario it was designed for) - Remove scaffold.go, scaffold_test.go, Makefile references - Remove unwired confidence_threshold from post-explore.sh dispatch - Fix agent docs: control labels now match implementation, auto_create default corrected to false Signed-off-by: Adam Scerra Co-authored-by: Cursor --- Makefile | 1 - docs/agents/critique.md | 17 +- docs/agents/explore.md | 5 +- docs/agents/refine.md | 5 +- .../fullsend-repo/scripts/post-explore.sh | 5 - .../scripts/sanitize-artifacts-test.sh | 119 -------- .../scripts/sanitize-artifacts.sh | 287 ------------------ internal/scaffold/scaffold.go | 2 - internal/scaffold/scaffold_test.go | 12 - 9 files changed, 12 insertions(+), 441 deletions(-) delete mode 100755 internal/scaffold/fullsend-repo/scripts/sanitize-artifacts-test.sh delete mode 100755 internal/scaffold/fullsend-repo/scripts/sanitize-artifacts.sh diff --git a/Makefile b/Makefile index 6357cbfb4..bced3a2e2 100644 --- a/Makefile +++ b/Makefile @@ -119,7 +119,6 @@ script-test: bash internal/scaffold/fullsend-repo/scripts/post-refine-test.sh bash internal/scaffold/fullsend-repo/scripts/post-critique-test.sh bash internal/scaffold/fullsend-repo/scripts/create-children-test.sh - bash internal/scaffold/fullsend-repo/scripts/sanitize-artifacts-test.sh python3 internal/scaffold/fullsend-repo/scripts/process-fix-result-test.py python3 skills/topissues/scripts/topissues_test.py diff --git a/docs/agents/critique.md b/docs/agents/critique.md index 13c323059..96aae3c81 100644 --- a/docs/agents/critique.md +++ b/docs/agents/critique.md @@ -23,11 +23,14 @@ Nothing gets created until this agent approves. ## Control labels -| Label | Meaning | -|-------|---------| -| `fullsend:critiquing` | Applied by the critique pre-script while evaluation is in progress. Removed on completion. | -| `fullsend:approved` | Applied by the critique post-script when the plan receives an `approved` verdict. | -| `fullsend:needs-input` | Applied by the critique post-script when human input is required before the pipeline can proceed. | +Labels are applied by `post-critique.sh` based on the verdict (GitHub flow only): + +| Label | When applied | +|-------|-------------| +| `refine-approved` | Plan approved (with `auto_create=false`), or max review rounds reached (escalation). | +| `refine-needs-input` | Human input is required before the pipeline can proceed (`needs_input` verdict). | +| `refine-needs-human` | Max review rounds reached — critique still has concerns, human must decide. | +| `refine-stalled` | Max review rounds reached — added alongside `refine-needs-human` to signal the pipeline has stopped. | ## Configuration and extension @@ -46,8 +49,8 @@ See [Customizing with AGENTS.md](../guides/user/customizing-with-agents-md.md) a When the critique agent approves a plan: -- **`auto_create=true`** (default): The post-script automatically creates child issues in the target tracker (GitHub or Jira). -- **`auto_create=false`**: The post-script posts an approval comment with the proposed plan. A human must comment `/fs-create` to trigger issue creation. +- **`auto_create=true`**: The post-script automatically creates child issues in the target tracker (GitHub or Jira). +- **`auto_create=false`** (default): The post-script posts an approval comment with the proposed plan. A human must comment `/fs-create` to trigger issue creation. This gives teams control over whether refinement is fully automated or requires human sign-off before backlog changes. diff --git a/docs/agents/explore.md b/docs/agents/explore.md index df0178bd5..c44761823 100644 --- a/docs/agents/explore.md +++ b/docs/agents/explore.md @@ -33,10 +33,7 @@ For Jira issues, the pipeline can also be triggered by: ## Control labels -| Label | Meaning | -|-------|---------| -| `fullsend:exploring` | Applied by the explore pre-script while exploration is in progress. Removed on completion. | -| `fullsend:explored` | Applied by the explore post-script when exploration completes successfully. Signals that refine can proceed. | +The explore agent does not manage any labels. On completion, it chains directly to the [refine agent](refine.md) via `workflow_dispatch`. ## Configuration and extension diff --git a/docs/agents/refine.md b/docs/agents/refine.md index f0eae14de..d151d0dde 100644 --- a/docs/agents/refine.md +++ b/docs/agents/refine.md @@ -30,10 +30,7 @@ There is no separate command to invoke refine alone. It runs as the second stage ## Control labels -| Label | Meaning | -|-------|---------| -| `fullsend:refining` | Applied by the refine pre-script while refinement is in progress. Removed on completion. | -| `fullsend:refined` | Applied by the refine post-script when a plan is produced. Signals that critique can proceed. | +The refine agent does not add labels. On completion, it chains directly to the [critique agent](critique.md) via `workflow_dispatch`. The post-script removes `refine-needs-input` and `human-refinement` labels (if present) as cleanup from prior runs. ## Configuration and extension diff --git a/internal/scaffold/fullsend-repo/scripts/post-explore.sh b/internal/scaffold/fullsend-repo/scripts/post-explore.sh index e6681e2bd..d6054239a 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-explore.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-explore.sh @@ -80,11 +80,6 @@ if [[ -n "$THIS_RUN_ID" ]]; then CHAIN_ARGS+=(-f auto_create="true") fi - # Pass through confidence threshold if explicitly set - if [[ -n "${REFINE_CONFIDENCE_THRESHOLD:-}" ]]; then - CHAIN_ARGS+=(-f confidence_threshold="${REFINE_CONFIDENCE_THRESHOLD}") - fi - gh workflow run refine.yml "${CHAIN_ARGS[@]}" \ 2>/dev/null || echo "::warning::Failed to chain refine workflow — trigger manually" pe_end "post-explore" "chain-refine" "$(jq -nc --arg run_id "$THIS_RUN_ID" --arg traceparent "$CURRENT_TRACEPARENT" '{explore_run_id:$run_id, traceparent:$traceparent}')" diff --git a/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts-test.sh b/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts-test.sh deleted file mode 100755 index bf289ab45..000000000 --- a/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts-test.sh +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env bash -# sanitize-artifacts-test.sh — Test sanitize-artifacts.sh redaction logic. -# -# Run from the repo root: bash internal/scaffold/fullsend-repo/scripts/sanitize-artifacts-test.sh - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SANITIZE_SCRIPT="${SCRIPT_DIR}/sanitize-artifacts.sh" -FAILURES=0 - -TEST_TMPDIR="$(mktemp -d)" -trap 'rm -rf "${TEST_TMPDIR}"' EXIT - -# --- Fixtures --- - -# Transcript JSONL with private data -TRANSCRIPT_FIXTURE='{"message":{"content":[{"type":"tool_result","content":"{\"source\":\"jira\",\"key\":\"PROJ-123\",\"summary\":\"Add login\",\"description\":\"Private issue body with secrets\",\"reporter\":\"alice@example.com\",\"host\":\"redhat.atlassian.net\",\"comments\":[{\"author\":\"bob@corp.com\",\"body\":\"internal comment\"}],\"linked_issues\":[{\"key\":\"PROJ-456\",\"description\":\"linked desc\"}],\"parent\":{\"key\":\"PROJ-100\",\"description\":\"parent body\"}}"},{"type":"text","text":"User alice@example.com reported this on redhat.atlassian.net"},{"type":"thinking","thinking":"I see https://redhat-internal.slack.com/archives/C123 is related"},{"type":"tool_use","input":{"query":"check https://docs.google.com/document/d/secret123"}}]}}' - -# agent-result.json with emails -RESULT_FIXTURE='{ - "summary": "Found issue at staging.atlassian.net reported by dev@company.com", - "confidence": {"overall": 85}, - "technical_landscape": { - "overview": "Check https://redhat-internal.slack.com/foo for details" - } -}' - -run_test() { - local test_name="$1" - - local input_dir="${TEST_TMPDIR}/input-${test_name}" - local output_dir="${TEST_TMPDIR}/output-${test_name}" - mkdir -p "${input_dir}/iteration-1/output" - - echo "${TRANSCRIPT_FIXTURE}" > "${input_dir}/iteration-1/transcript.jsonl" - echo "${RESULT_FIXTURE}" > "${input_dir}/iteration-1/output/agent-result.json" - - local exit_code=0 - bash "${SANITIZE_SCRIPT}" "${input_dir}" "${output_dir}" > "${TEST_TMPDIR}/stdout-${test_name}.log" 2>&1 || exit_code=$? - - if [[ ${exit_code} -ne 0 ]]; then - echo "FAIL: ${test_name} — exit code ${exit_code}" - cat "${TEST_TMPDIR}/stdout-${test_name}.log" - FAILURES=$((FAILURES + 1)) - return - fi - - echo "PASS: ${test_name}" -} - -assert_file_contains() { - local test_name="$1" file="$2" pattern="$3" - if ! grep -qF "${pattern}" "${file}"; then - echo "FAIL: ${test_name} — expected '${pattern}' in $(basename "${file}")" - FAILURES=$((FAILURES + 1)) - fi -} - -assert_file_not_contains() { - local test_name="$1" file="$2" pattern="$3" - if grep -qF "${pattern}" "${file}"; then - echo "FAIL: ${test_name} — unexpected '${pattern}' in $(basename "${file}")" - FAILURES=$((FAILURES + 1)) - fi -} - -# --- Tests --- - -# 1. Transcript redaction -run_test "transcript-redaction" -SANITIZED_TRANSCRIPT="${TEST_TMPDIR}/output-transcript-redaction/iteration-1/transcript.jsonl" - -# Emails should be redacted -assert_file_not_contains "transcript-emails" "$SANITIZED_TRANSCRIPT" "alice@example.com" -assert_file_not_contains "transcript-emails" "$SANITIZED_TRANSCRIPT" "bob@corp.com" -assert_file_contains "transcript-emails" "$SANITIZED_TRANSCRIPT" "[redacted-email]" - -# Atlassian hosts should be redacted -assert_file_not_contains "transcript-hosts" "$SANITIZED_TRANSCRIPT" "redhat.atlassian.net" -assert_file_contains "transcript-hosts" "$SANITIZED_TRANSCRIPT" "[redacted-host]" - -# Issue context fields should be redacted -assert_file_not_contains "transcript-body" "$SANITIZED_TRANSCRIPT" "Private issue body with secrets" -assert_file_contains "transcript-body" "$SANITIZED_TRANSCRIPT" "[redacted" - -# Internal URLs should be redacted -assert_file_not_contains "transcript-slack" "$SANITIZED_TRANSCRIPT" "redhat-internal.slack.com" -assert_file_not_contains "transcript-gdocs" "$SANITIZED_TRANSCRIPT" "docs.google.com" - -# Structural metadata should survive -assert_file_contains "transcript-metadata" "$SANITIZED_TRANSCRIPT" "PROJ-123" -assert_file_contains "transcript-metadata" "$SANITIZED_TRANSCRIPT" "Add login" - -# 2. agent-result.json redaction (text scrub only) -SANITIZED_RESULT="${TEST_TMPDIR}/output-transcript-redaction/iteration-1/output/agent-result.json" - -assert_file_not_contains "result-emails" "$SANITIZED_RESULT" "dev@company.com" -assert_file_not_contains "result-hosts" "$SANITIZED_RESULT" "staging.atlassian.net" -assert_file_not_contains "result-slack" "$SANITIZED_RESULT" "redhat-internal.slack.com" -assert_file_contains "result-structure" "$SANITIZED_RESULT" "confidence" -assert_file_contains "result-structure" "$SANITIZED_RESULT" "overall" - -# 3. Missing arguments should fail -if bash "${SANITIZE_SCRIPT}" 2>/dev/null; then - echo "FAIL: missing-args — expected failure" - FAILURES=$((FAILURES + 1)) -else - echo "PASS: missing-args (expected failure)" -fi - -if [[ ${FAILURES} -gt 0 ]]; then - echo "" - echo "${FAILURES} test(s) failed." - exit 1 -fi - -echo "" -echo "All sanitize-artifacts tests passed." diff --git a/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts.sh b/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts.sh deleted file mode 100755 index 234bce6b3..000000000 --- a/internal/scaffold/fullsend-repo/scripts/sanitize-artifacts.sh +++ /dev/null @@ -1,287 +0,0 @@ -#!/usr/bin/env bash -# sanitize-artifacts.sh — Redact private data from agent artifacts before upload. -# -# Keeps transcript structure intact for debugging/tracing while scrubbing: -# - Jira issue descriptions, comment bodies, reporter emails -# - Internal hostnames (*.atlassian.net) -# - Email addresses -# - Parent/linked issue descriptions -# - Internal URLs (Slack, Google Docs, etc.) -# -# What survives: -# - Agent reasoning and tool call commands -# - Issue keys, summaries, statuses, labels (structural metadata) -# - Phase markers and timing -# - agent-result.json (the agent's synthesis, not raw input) -# - exploration_context.json -# -# Usage: sanitize-artifacts.sh - -set -euo pipefail - -OUTPUT_DIR="${1:?Usage: sanitize-artifacts.sh }" -SANITIZED_DIR="${2:?Usage: sanitize-artifacts.sh }" - -mkdir -p "$SANITIZED_DIR" - -python3 - "$OUTPUT_DIR" "$SANITIZED_DIR" <<'PYTHON_SCRIPT' -import json -import os -import re -import sys - -output_dir = sys.argv[1] -sanitized_dir = sys.argv[2] - -EMAIL_RE = re.compile(r'[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}') -ATLASSIAN_HOST_RE = re.compile(r'[a-zA-Z0-9\-]+\.atlassian\.net') -INTERNAL_URL_RE = re.compile(r'https?://redhat-internal\.slack\.com/\S+') -GDOCS_URL_RE = re.compile(r'https?://docs\.google\.com/\S+') - -REDACTED_EMAIL = '[redacted-email]' -REDACTED_HOST = '[redacted-host]' -REDACTED_BODY = '[redacted — private issue content]' -REDACTED_URL = '[redacted-internal-url]' - -redaction_counts = { - 'emails': 0, - 'hosts': 0, - 'internal_urls': 0, - 'gdocs_urls': 0, - 'issue_descriptions': 0, - 'issue_comments': 0, - 'issue_reporters': 0, -} - -def looks_like_issue_context(obj): - if not isinstance(obj, dict): - return False - return ('source' in obj and 'key' in obj - and ('description' in obj or 'summary' in obj) - and obj.get('source') in ('jira', 'github')) - -def redact_issue_context(obj): - global redaction_counts - obj = dict(obj) - - for field in ('description', 'body'): - if field in obj and obj[field]: - obj[field] = REDACTED_BODY - redaction_counts['issue_descriptions'] += 1 - - if 'reporter' in obj: - obj['reporter'] = REDACTED_EMAIL - redaction_counts['issue_reporters'] += 1 - - if 'host' in obj: - obj['host'] = REDACTED_HOST - - if 'parent' in obj and isinstance(obj['parent'], dict): - parent = dict(obj['parent']) - for field in ('description', 'body'): - if field in parent: - parent[field] = REDACTED_BODY - redaction_counts['issue_descriptions'] += 1 - obj['parent'] = parent - - if 'comments' in obj and isinstance(obj['comments'], list): - for c in obj['comments']: - if isinstance(c, dict): - redaction_counts['issue_comments'] += 1 - obj['comments'] = [ - {**c, 'body': REDACTED_BODY, 'author': REDACTED_EMAIL} - if isinstance(c, dict) else c - for c in obj['comments'] - ] - - if 'linked_issues' in obj and isinstance(obj['linked_issues'], list): - redacted = [] - for li in obj['linked_issues']: - if isinstance(li, dict): - li = {**li} - if 'description' in li: - li['description'] = REDACTED_BODY - redaction_counts['issue_descriptions'] += 1 - redacted.append(li) - obj['linked_issues'] = redacted - - return obj - -def scrub_text(text): - global redaction_counts - redaction_counts['emails'] += len(EMAIL_RE.findall(text)) - redaction_counts['hosts'] += len(ATLASSIAN_HOST_RE.findall(text)) - redaction_counts['internal_urls'] += len(INTERNAL_URL_RE.findall(text)) - redaction_counts['gdocs_urls'] += len(GDOCS_URL_RE.findall(text)) - text = EMAIL_RE.sub(REDACTED_EMAIL, text) - text = ATLASSIAN_HOST_RE.sub(REDACTED_HOST, text) - text = INTERNAL_URL_RE.sub(REDACTED_URL, text) - text = GDOCS_URL_RE.sub(REDACTED_URL, text) - return text - -def find_json_object(text, start=0): - """Find the next top-level JSON object in text starting from start.""" - brace_start = text.find('{', start) - if brace_start == -1: - return None, None, None - - depth = 0 - in_string = False - escape_next = False - - for i in range(brace_start, len(text)): - c = text[i] - if escape_next: - escape_next = False - continue - if c == '\\' and in_string: - escape_next = True - continue - if c == '"' and not escape_next: - in_string = not in_string - continue - if in_string: - continue - if c == '{': - depth += 1 - elif c == '}': - depth -= 1 - if depth == 0: - return brace_start, i + 1, text[brace_start:i + 1] - - return None, None, None - -def redact_content(content_str): - """Redact issue-context JSON blocks from tool output (may be mixed content).""" - if not isinstance(content_str, str): - return content_str - - result_parts = [] - pos = 0 - found_any = False - - while pos < len(content_str): - start, end, json_str = find_json_object(content_str, pos) - - if start is None: - result_parts.append(scrub_text(content_str[pos:])) - break - - result_parts.append(scrub_text(content_str[pos:start])) - - try: - parsed = json.loads(json_str) - if looks_like_issue_context(parsed): - redacted = redact_issue_context(parsed) - result_parts.append(json.dumps(redacted, indent=2)) - found_any = True - else: - result_parts.append(scrub_text(json_str)) - except (json.JSONDecodeError, ValueError): - result_parts.append(scrub_text(json_str)) - - pos = end - - return ''.join(result_parts) - -def redact_jsonl_line(line_str): - try: - obj = json.loads(line_str) - except (json.JSONDecodeError, ValueError): - return scrub_text(line_str) - - msg = obj.get('message', {}) - contents = msg.get('content', []) - - if isinstance(contents, list): - for item in contents: - if not isinstance(item, dict): - continue - if item.get('type') == 'tool_result': - content = item.get('content', '') - if isinstance(content, str): - item['content'] = redact_content(content) - if item.get('type') == 'text': - text = item.get('text', '') - if isinstance(text, str): - item['text'] = scrub_text(text) - if item.get('type') == 'thinking': - thinking = item.get('thinking', '') - if isinstance(thinking, str): - item['thinking'] = scrub_text(thinking) - if item.get('type') == 'tool_use': - inp = item.get('input', {}) - if isinstance(inp, dict): - for k, v in inp.items(): - if isinstance(v, str): - inp[k] = scrub_text(v) - - tool_result = obj.get('toolUseResult', {}) - if isinstance(tool_result, dict): - for field in ('stdout', 'stderr', 'content'): - val = tool_result.get(field, '') - if isinstance(val, str) and val: - tool_result[field] = redact_content(val) - - return json.dumps(obj, ensure_ascii=False) - -transcript_count = 0 -result_count = 0 - -for root, dirs, files in os.walk(output_dir): - for fname in files: - src = os.path.join(root, fname) - rel = os.path.relpath(src, output_dir) - dest = os.path.join(sanitized_dir, rel) - os.makedirs(os.path.dirname(dest), exist_ok=True) - - if fname.endswith('.jsonl'): - with open(src, 'r') as f_in, open(dest, 'w') as f_out: - for line in f_in: - line = line.rstrip('\n') - if line: - f_out.write(redact_jsonl_line(line) + '\n') - transcript_count += 1 - - elif fname == 'agent-result.json': - with open(src, 'r') as f_in, open(dest, 'w') as f_out: - content = f_in.read() - f_out.write(scrub_text(content)) - result_count += 1 - - # Skip sandbox logs (openshell-gateway.log, openshell-sandbox.log) - -print(f'Sanitized {transcript_count} transcript(s), {result_count} result(s)') -print(f'Skipped sandbox logs (gateway/sandbox .log files)') - -# Write redaction summary -total = sum(redaction_counts.values()) -summary = { - 'total_redactions': total, - 'categories': {k: v for k, v in redaction_counts.items() if v > 0}, - 'files_processed': {'transcripts': transcript_count, 'results': result_count}, -} - -summary_path = os.path.join(sanitized_dir, 'redaction-summary.json') -with open(summary_path, 'w') as f: - json.dump(summary, f, indent=2) - -if total > 0: - print(f'⚠️ PII redaction summary: {total} redaction(s) applied') - for cat, count in sorted(redaction_counts.items()): - if count > 0: - print(f' - {cat}: {count}') - print(f'Details written to: redaction-summary.json') -else: - print('No PII redactions needed.') -PYTHON_SCRIPT - -if [[ -f "$SANITIZED_DIR/redaction-summary.json" ]]; then - TOTAL=$(jq -r '.total_redactions' "$SANITIZED_DIR/redaction-summary.json") - if [[ "$TOTAL" -gt 0 ]]; then - echo "::warning::PII sanitization applied ${TOTAL} redaction(s). See redaction-summary.json in artifacts for details." - fi -fi - -echo "Sanitize complete. Contents:" -find "$SANITIZED_DIR" -type f | sort diff --git a/internal/scaffold/scaffold.go b/internal/scaffold/scaffold.go index be1a756a1..21b4e5084 100644 --- a/internal/scaffold/scaffold.go +++ b/internal/scaffold/scaffold.go @@ -52,12 +52,10 @@ var executableFiles = map[string]struct{}{ "scripts/pipeline-events.sh": {}, "scripts/markdown-to-adf.py": {}, "scripts/pipeline-helpers.sh": {}, - "scripts/sanitize-artifacts.sh": {}, "scripts/post-explore-test.sh": {}, "scripts/post-refine-test.sh": {}, "scripts/post-critique-test.sh": {}, "scripts/create-children-test.sh": {}, - "scripts/sanitize-artifacts-test.sh": {}, } // FileMode returns the Git tree mode for a scaffold file. diff --git a/internal/scaffold/scaffold_test.go b/internal/scaffold/scaffold_test.go index e2cc37f1a..38eaa54fa 100644 --- a/internal/scaffold/scaffold_test.go +++ b/internal/scaffold/scaffold_test.go @@ -117,7 +117,6 @@ func TestFullsendRepoFilesExist(t *testing.T) { "scripts/pipeline-events.sh", "scripts/pipeline-helpers.sh", "scripts/markdown-to-adf.py", - "scripts/sanitize-artifacts.sh", "skills/jira-read/SKILL.md", "skills/public-research/SKILL.md", ".github/workflows/explore.yml", @@ -1195,17 +1194,6 @@ func TestPipelineEventsContent(t *testing.T) { assert.Contains(t, s, "pipeline-events.jsonl") } -func TestSanitizeArtifactsContent(t *testing.T) { - content, err := FullsendRepoFile("scripts/sanitize-artifacts.sh") - require.NoError(t, err) - s := string(content) - assert.Contains(t, s, "redact") - assert.Contains(t, s, "atlassian.net") - assert.Contains(t, s, "redacted-email") - assert.Contains(t, s, "redacted-host") - assert.Contains(t, s, "agent-result.json") -} - func TestCreateChildrenScriptContent(t *testing.T) { content, err := FullsendRepoFile("scripts/create-children.sh") require.NoError(t, err) From 6b8fed42df2536d3c2053476a894f056f0f7af2e Mon Sep 17 00:00:00 2001 From: Adam Scerra Date: Thu, 18 Jun 2026 15:56:56 -0400 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20address=20review=20findings=20?= =?UTF-8?q?=E2=80=94=20fail-closed,=20injection=20guards,=20ADR=20reframe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Change REPO_VISIBILITY fallback from "unknown" to "public" in pre-explore.sh, jira-dispatch.yml, jira-comment-poller.yml so gh api failures trigger the safety block (fail-closed) - Add ::stop-commands:: guards around user-controlled echo blocks in refine-dispatch.yml and jira-dispatch.yml to prevent GHA command injection - Convert jq|while pipes to process substitution in jira-comment-poller.yml so DISPATCHED counter propagates to parent shell - Standardize ERROR: prefixes to ::error:: annotations in pre-explore.sh - Reframe ADR 0051 per Ralph's feedback: decision is agent chaining as a new orchestration pattern, not adding 3 agents. Add Alternatives Considered section with single-workflow pipeline (#234, #1817), issue-comment data store, and repository_dispatch options Signed-off-by: Adam Scerra Co-authored-by: Cursor --- .../0051-refinement-pipeline-architecture.md | 68 +++++++++++-------- .../.github/workflows/jira-comment-poller.yml | 10 +-- .../.github/workflows/jira-dispatch.yml | 8 ++- .../.github/workflows/refine-dispatch.yml | 3 + .../fullsend-repo/scripts/pre-explore.sh | 14 ++-- 5 files changed, 63 insertions(+), 40 deletions(-) diff --git a/docs/ADRs/0051-refinement-pipeline-architecture.md b/docs/ADRs/0051-refinement-pipeline-architecture.md index f37843dd0..90cdb1e2c 100644 --- a/docs/ADRs/0051-refinement-pipeline-architecture.md +++ b/docs/ADRs/0051-refinement-pipeline-architecture.md @@ -17,53 +17,67 @@ Accepted ## Context -fullsend currently ships 6 standalone agents (triage, code, review, fix, retro, prioritize), each triggered by a single event and producing a single output. The Feature Refinement Working Group has developed and validated a multi-stage pipeline that decomposes high-level issues into implementable work items through exploration, refinement, and quality-gated critique. +fullsend currently ships 6 standalone agents (triage, code, review, fix, retro, prioritize). Each is triggered by a single event and produces a single output. When agents relate to each other today, the coupling is loose: triage adds a `ready-to-code` label, which fires a webhook, and the code agent reads the issue fresh from GitHub. No data passes between them — each agent is self-contained. -This pipeline extends fullsend's existing patterns: +The Feature Refinement Working Group needs a pipeline that decomposes high-level issues into implementable work items through three stages: exploration, refinement, and quality-gated critique. This pipeline cannot work with fullsend's current standalone-agent model because each stage produces structured context that the next stage consumes: -1. **Chained agents with artifact passing** (new): Existing agents are standalone — triage triggers code via a `ready-to-code` label, which fires a webhook, and the code agent reads the issue fresh from GitHub. No data passes between them. The refinement pipeline is different because each stage produces structured context that the next stage needs: explore produces `exploration_context.json` (~50-100KB of analyzed research), refine produces `refine-result.json` (the full decomposition plan with children, acceptance criteria, etc.), and critique needs both to evaluate the plan. Labels can signal "go/no-go" but can't carry a run ID for artifact correlation, so we use explicit `workflow_dispatch` chaining instead. For example, post-explore.sh chains to refine like this: +- **explore** produces `exploration_context.json` (~50-100KB of analyzed research, codebase findings, prior art) +- **refine** consumes that context and produces `refine-result.json` (the full decomposition plan with children, acceptance criteria, etc.) +- **critique** consumes both to evaluate the plan against the original issue - ``` - gh workflow run refine.yml \ - --repo "$WORKFLOW_REPO" \ - -f issue_key="PROJ-123" \ - -f issue_source="jira" \ - -f explore_run_id="$GITHUB_RUN_ID" - ``` +Labels can signal "go/no-go" but cannot carry the structured data or the run ID needed to locate a previous stage's artifacts. This means the existing pattern of loose coupling via labels + webhooks is insufficient — we need a way for agents to explicitly chain to each other and pass data between runs. - The refine pre-script then downloads explore's artifact using that run ID: +## Decision - ``` - gh run download "$EXPLORE_RUN_ID" \ - --repo "$REPO" \ - --name "fullsend-explore" \ - --dir "$ARTIFACT_DIR" - ``` +We will introduce **agent chaining** as a new orchestration pattern in fullsend: the ability for one agent's post-script to trigger a subsequent agent via `workflow_dispatch`, passing a run ID that the next agent uses to download the previous stage's artifacts. -2. **Revision loops** (extension of review → fix): Critique can send work back to refine for up to N rounds (default 3) before escalating to a human. This mirrors the existing review → fix iteration pattern (`FIX_ITERATION` cap, default 5) but uses `workflow_dispatch` chaining instead of PR event triggers, since the critique feedback artifact needs to be passed back to refine. +The first use of this pattern is the refinement pipeline — three new agent roles (**explore**, **refine**, **critique**) that chain together to decompose issues. But the pattern itself is general-purpose and available for future multi-stage workflows. -The pipeline has been validated in fullsend-ai/features as custom agents over several weeks with consistent quality improvements in issue decomposition and reduced manual refinement time. +### How chaining works -## Decision +A post-script chains to the next stage by invoking `gh workflow run` with the current run ID: + +``` +gh workflow run refine.yml \ + --repo "$WORKFLOW_REPO" \ + -f issue_key="PROJ-123" \ + -f issue_source="jira" \ + -f explore_run_id="$GITHUB_RUN_ID" +``` -We will add three new agent roles to the fullsend scaffold: **explore**, **refine**, and **critique**. These agents form a single refinement pipeline, triggered by the `/fs-refine` command on an issue. +The downstream pre-script downloads the upstream artifact using that run ID: -This is a **Phase 1 implementation** focused on getting the pipeline functional and available to all fullsend installations. Later phases will harden, optimize, and extend it based on production feedback. +``` +gh run download "$EXPLORE_RUN_ID" \ + --repo "$REPO" \ + --name "fullsend-explore" \ + --dir "$ARTIFACT_DIR" +``` + +This extends the existing review → fix iteration pattern (which uses `FIX_ITERATION` cap, default 5, with PR event triggers) to support `workflow_dispatch`-based loops where critique feedback artifacts must be passed back to refine. ### Phase 1 decisions (this PR) +This is a **Phase 1 implementation** focused on getting the chaining pattern and refinement pipeline functional. Later phases will harden, optimize, and extend based on production feedback. + **Separate GitHub Apps**: Each agent (explore, refine, critique) gets its own GitHub App, consistent with every other fullsend agent. For the GitHub workflow, this provides a clear audit trail — each agent's comments and actions appear under a distinct bot identity. It also enables granular access control and avoids coupling permissions if the agents' needs diverge. Note: for the Jira workflow, all three agents share the same `JIRA_EMAIL` service account, so Jira comments are attributed to a single bot user regardless of which agent posted them. **Default enabled, command-gated**: The agents ship with every fullsend installation but only activate when a user explicitly invokes `/fs-refine`. There are no automatic triggers — the pipeline never runs unless a human requests it. -**Pipeline chaining via workflow_dispatch** (not event-driven): Existing agent chains use labels + webhooks (e.g., triage adds `ready-to-code` → code agent triggers on `issues.labeled`). This works when agents are independent — code doesn't need triage's output, it reads the issue directly. The refinement pipeline uses explicit `gh workflow run` instead because each stage needs the previous stage's artifact, identified by run ID. Labels can't carry this correlation data. - **Revision loop with hard cap**: Critique can return a "revise" verdict, causing post-critique.sh to re-trigger refine.yml with critique feedback as additional context. MAX_REVIEW_ROUNDS (default 3) prevents infinite loops. If the cap is reached, the plan is presented to a human for final decision. **Separate dispatch workflows**: The pipeline uses separate dispatch files (refine-dispatch.yml, jira-dispatch.yml) rather than integrating into the existing dispatch.yml. We chose this because it matches the validated pattern from fullsend-ai/features and avoids modifying the shared dispatch routing logic in Phase 1. **No eval system**: The features repo has a full eval system (MLflow baselines, fixture evals, eval-gate.yml). This is intentionally excluded from Phase 1 to reduce PR scope and review burden. +### Alternatives considered + +**Single-workflow pipeline**: Run all three stages as sequential jobs within one GitHub Actions workflow. Data passes via job outputs or workspace sharing — no cross-run artifact downloads, no run-ID correlation, and artifacts never leave the run (eliminating the data leakage risk for public repos entirely). This would also reduce the GitHub App count from 3 to 1, since all stages run under a single workflow identity. We deferred this because the harness currently expects one agent = one workflow run, and changing that is a significant architectural lift. It also means a failure in stage 3 requires re-running the entire pipeline, and individual agent actions are no longer attributable in the GitHub UI. This remains the strongest candidate for Phase 2 (see Deferred table below). Related existing work: #234 (design validation loop and multi-agent orchestration patterns for harness definitions) and #1817 (investigate deterministic orchestration for intra-stage multi-agent pipelines). + +**Issue comments as data store**: Write structured context into issue comments (e.g., a machine-readable JSON block) instead of artifacts, so downstream agents read the issue like every other fullsend agent. This is closest to fullsend's current model. We rejected it because GitHub comments have a 65,536-character limit and exploration context can reach 50-100KB, making data truncation likely. It also pollutes the issue with large machine-readable blobs that aren't useful to humans. + +**Repository dispatch with client payload**: Use `repository_dispatch` events with a structured `client_payload` instead of `workflow_dispatch` inputs. Similar to our chosen approach, but `client_payload` is limited to 10 top-level properties and has payload size constraints that would require compression or external storage for larger artifacts — adding complexity without clear benefit over `workflow_dispatch` + run-ID correlation. + ### Deferred to Phase 2+ | Decision | Rationale for deferral | @@ -78,16 +92,16 @@ This is a **Phase 1 implementation** focused on getting the pipeline functional ### Positive +- Agent chaining is a general-purpose pattern — future multi-stage workflows (e.g., plan → implement → validate) can reuse the same `workflow_dispatch` + run-ID artifact correlation mechanism without inventing new infrastructure - Teams get automated issue decomposition from day one after installation - The pipeline is entirely opt-in (requires explicit `/fs-refine` command) -- Chaining pattern is reusable for future multi-stage workflows - Revision loop ensures quality without human intervention for routine work ### Negative +- Agent chaining introduces coupling between agents that didn't exist before — downstream agents depend on upstream artifact structure, naming, and availability - Three new roles (each with its own GitHub App) increase the total agent count from 7 to 10 and add 3 apps to install -- Pipeline chaining is a new pattern that adds complexity to the scaffold -- Artifact-based data passing has a 90-day retention limit +- Artifact-based data passing has a 90-day retention limit (GitHub Actions constraint) - Separate dispatch files add workflow sprawl (mitigated by planned consolidation in Phase 2) ### Risks diff --git a/internal/scaffold/fullsend-repo/.github/workflows/jira-comment-poller.yml b/internal/scaffold/fullsend-repo/.github/workflows/jira-comment-poller.yml index 4f9438329..40b1c752c 100644 --- a/internal/scaffold/fullsend-repo/.github/workflows/jira-comment-poller.yml +++ b/internal/scaffold/fullsend-repo/.github/workflows/jira-comment-poller.yml @@ -39,7 +39,7 @@ jobs: # Public Jira projects on public repos are fine — set JIRA_PROJECT_VISIBILITY=public. JIRA_VIS="${JIRA_PROJECT_VISIBILITY:-private}" if [[ "$JIRA_VIS" != "public" ]]; then - REPO_VISIBILITY=$(gh api "repos/${REPO}" --jq '.visibility' 2>/dev/null || echo "unknown") + REPO_VISIBILITY=$(gh api "repos/${REPO}" --jq '.visibility' 2>/dev/null || echo "public") if [[ "$REPO_VISIBILITY" == "public" ]]; then echo "::error::SECURITY: Private Jira poller blocked — repo ${REPO} is public." echo "::error::Private Jira data would leak into public artifacts/logs." @@ -74,13 +74,13 @@ jobs: DISPATCHED=0 - echo "$ISSUES" | jq -r '.issues[].key' | while read -r ISSUE_KEY; do + while read -r ISSUE_KEY; do echo "" echo "--- Checking $ISSUE_KEY ---" COMMENTS=$(jira_api GET "/rest/api/3/issue/${ISSUE_KEY}/comment?orderBy=-created&maxResults=20") - echo "$COMMENTS" | jq -r '.comments[] | @base64' | while read -r COMMENT_B64; do + while read -r COMMENT_B64; do COMMENT_ID=$(echo "$COMMENT_B64" | base64 -d | jq -r '.id') AUTHOR=$(echo "$COMMENT_B64" | base64 -d | jq -r '.author.displayName // .author.emailAddress // "unknown"') CREATED=$(echo "$COMMENT_B64" | base64 -d | jq -r '.created') @@ -142,8 +142,8 @@ jobs: echo " → Dispatched and acknowledged" DISPATCHED=$((DISPATCHED + 1)) - done - done + done < <(echo "$COMMENTS" | jq -r '.comments[] | @base64') + done < <(echo "$ISSUES" | jq -r '.issues[].key') echo "" echo "=== Polling complete ===" diff --git a/internal/scaffold/fullsend-repo/.github/workflows/jira-dispatch.yml b/internal/scaffold/fullsend-repo/.github/workflows/jira-dispatch.yml index 1a95f6418..d2a4ac3cb 100644 --- a/internal/scaffold/fullsend-repo/.github/workflows/jira-dispatch.yml +++ b/internal/scaffold/fullsend-repo/.github/workflows/jira-dispatch.yml @@ -56,7 +56,7 @@ jobs: # Public Jira projects on public repos are fine — set JIRA_PROJECT_VISIBILITY=public. JIRA_VIS="${JIRA_PROJECT_VISIBILITY:-private}" if [[ "$JIRA_VIS" != "public" ]]; then - REPO_VISIBILITY=$(gh api "repos/${REPO}" --jq '.visibility' 2>/dev/null || echo "unknown") + REPO_VISIBILITY=$(gh api "repos/${REPO}" --jq '.visibility' 2>/dev/null || echo "public") if [[ "$REPO_VISIBILITY" == "public" ]]; then echo "::error::SECURITY: Private Jira dispatch blocked — repo ${REPO} is public." echo "::error::Private Jira data would leak into public artifacts/logs." @@ -76,11 +76,14 @@ jobs: exit 1 fi + STOP_TOKEN=$(uuidgen) + echo "::stop-commands::${STOP_TOKEN}" echo "Jira dispatch:" echo " Jira key: ${JIRA_KEY}" echo " Command: ${COMMAND}" echo " Full command: ${RAW_COMMAND}" echo " Event type: ${EVENT_TYPE}" + echo "::${STOP_TOKEN}::" case "$COMMAND" in @@ -99,10 +102,13 @@ jobs: CONTEXT_REF=$(printf '%s\n' "$RAW_COMMAND" | grep -oP '(?<=--context\s)\S+' || true) TARGET_REPO=$(printf '%s\n' "$RAW_COMMAND" | grep -oP '(?<=--repo\s)\S+' || true) + STOP_TOKEN2=$(uuidgen) + echo "::stop-commands::${STOP_TOKEN2}" echo " Skip explore: ${SKIP_EXPLORE}" echo " Auto-create: ${AUTO_CREATE}" echo " Context ref: ${CONTEXT_REF:-none}" echo " Target repo: ${TARGET_REPO:-none (web research only)}" + echo "::${STOP_TOKEN2}::" DISPATCH_ARGS=( -f issue_key="$JIRA_KEY" diff --git a/internal/scaffold/fullsend-repo/.github/workflows/refine-dispatch.yml b/internal/scaffold/fullsend-repo/.github/workflows/refine-dispatch.yml index a2d38cf2a..b6d35a427 100644 --- a/internal/scaffold/fullsend-repo/.github/workflows/refine-dispatch.yml +++ b/internal/scaffold/fullsend-repo/.github/workflows/refine-dispatch.yml @@ -129,12 +129,15 @@ jobs: CONTEXT_REF=$(printf '%s\n' "${COMMENT_BODY}" | grep -oP '(?<=--context\s)\S+' || true) fi + STOP_TOKEN=$(uuidgen) + echo "::stop-commands::${STOP_TOKEN}" echo "Dispatching GitHub issue refinement:" echo " Issue: #${ISSUE_NUMBER}" echo " Skip explore: ${SKIP_EXPLORE}" echo " Context ref: ${CONTEXT_REF:-none}" echo " Auto-create: ${AUTO_CREATE}" echo " Follow-up: ${IS_FOLLOWUP}" + echo "::${STOP_TOKEN}::" if [[ "$IS_FOLLOWUP" == "true" ]]; then EXPLORE_RUN_ID=$(gh run list --repo "$REPO" --workflow "fullsend-explore" \ diff --git a/internal/scaffold/fullsend-repo/scripts/pre-explore.sh b/internal/scaffold/fullsend-repo/scripts/pre-explore.sh index 9a04bcc48..c13982636 100755 --- a/internal/scaffold/fullsend-repo/scripts/pre-explore.sh +++ b/internal/scaffold/fullsend-repo/scripts/pre-explore.sh @@ -43,7 +43,7 @@ if [[ "${ISSUE_SOURCE}" == "jira" ]]; then if [[ "$JIRA_VIS" != "public" ]]; then CONFIG_REPO="${GITHUB_REPOSITORY:-}" if [[ -n "$CONFIG_REPO" ]]; then - REPO_VISIBILITY=$(gh api "repos/${CONFIG_REPO}" --jq '.visibility' 2>/dev/null || echo "unknown") + REPO_VISIBILITY=$(gh api "repos/${CONFIG_REPO}" --jq '.visibility' 2>/dev/null || echo "public") if [[ "$REPO_VISIBILITY" == "public" ]]; then echo "::error::SECURITY: Private Jira source blocked — this repo (${CONFIG_REPO}) is public." echo "::error::Private Jira data would leak into public workflow artifacts and logs." @@ -59,23 +59,23 @@ fi # Validate inputs to prevent injection if [[ "${ISSUE_SOURCE}" != "jira" && "${ISSUE_SOURCE}" != "github" ]]; then - echo "ERROR: ISSUE_SOURCE must be 'jira' or 'github', got: ${ISSUE_SOURCE}" + echo "::error::ISSUE_SOURCE must be 'jira' or 'github', got: ${ISSUE_SOURCE}" exit 1 fi if [[ "${ISSUE_SOURCE}" == "jira" && ! "${ISSUE_KEY}" =~ ^[A-Z][A-Z0-9]+-[0-9]+$ ]]; then - echo "ERROR: ISSUE_KEY does not match Jira key pattern (e.g., PROJECT-123): ${ISSUE_KEY}" + echo "::error::ISSUE_KEY does not match Jira key pattern (e.g., PROJECT-123): ${ISSUE_KEY}" exit 1 fi if [[ "${ISSUE_SOURCE}" == "github" && ! "${ISSUE_KEY}" =~ ^[0-9]+$ ]]; then - echo "ERROR: ISSUE_KEY must be a numeric GitHub issue number, got: ${ISSUE_KEY}" + echo "::error::ISSUE_KEY must be a numeric GitHub issue number, got: ${ISSUE_KEY}" exit 1 fi if [[ "${ISSUE_SOURCE}" == "jira" ]]; then if [[ -z "${JIRA_HOST:-}" || -z "${JIRA_EMAIL:-}" || -z "${JIRA_API_TOKEN:-}" ]]; then - echo "ERROR: Jira credentials not set (JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN)" + echo "::error::Jira credentials not set (JIRA_HOST, JIRA_EMAIL, JIRA_API_TOKEN)" exit 1 fi @@ -237,7 +237,7 @@ if [[ "${ISSUE_SOURCE}" == "jira" ]]; then elif [[ "${ISSUE_SOURCE}" == "github" ]]; then if [[ -z "${REPO_FULL_NAME:-}" ]]; then - echo "ERROR: REPO_FULL_NAME not set for GitHub source" + echo "::error::REPO_FULL_NAME not set for GitHub source" exit 1 fi @@ -312,7 +312,7 @@ elif [[ "${ISSUE_SOURCE}" == "github" ]]; then }' > "$WORKSPACE/issue-context.json" else - echo "ERROR: Unknown ISSUE_SOURCE: ${ISSUE_SOURCE}" + echo "::error::Unknown ISSUE_SOURCE: ${ISSUE_SOURCE}" exit 1 fi