diff --git a/.github/workflows/ai-fix.yml b/.github/workflows/ai-fix.yml index 87a54c9..8829306 100644 --- a/.github/workflows/ai-fix.yml +++ b/.github/workflows/ai-fix.yml @@ -68,15 +68,29 @@ jobs: echo "::warning::No CPA credentials; CPA fallback channel disabled (gateway only)." fi + - name: Detect gateway source + id: gw + shell: bash + env: + GATEWAY_BASE_URL: ${{ secrets.GATEWAY_BASE_URL }} + run: | + set -euo pipefail + # secrets context is not allowed in step `if:`; expose via outputs. + if [[ -n "${GATEWAY_BASE_URL}" ]]; then + echo "external=true" >> "$GITHUB_OUTPUT" + else + echo "external=false" >> "$GITHUB_OUTPUT" + fi + - name: Set up Node - if: secrets.GATEWAY_BASE_URL == '' + if: steps.gw.outputs.external != 'true' uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24 cache: npm - name: Start in-job gateway (free models, no login needed) - if: secrets.GATEWAY_BASE_URL == '' + if: steps.gw.outputs.external != 'true' shell: bash run: | set -euo pipefail diff --git a/.github/workflows/ai-review.yml b/.github/workflows/ai-review.yml index 453f872..abeed25 100644 --- a/.github/workflows/ai-review.yml +++ b/.github/workflows/ai-review.yml @@ -102,15 +102,30 @@ jobs: echo "head_sha=${head_sha}" } >> "$GITHUB_OUTPUT" + - name: Detect gateway source + id: gw + shell: bash + env: + GATEWAY_BASE_URL: ${{ secrets.GATEWAY_BASE_URL }} + run: | + set -euo pipefail + # secrets context is not allowed in step `if:`; expose via outputs. + # Always runs (trivial check) so downstream never reads an absent output. + if [[ -n "${GATEWAY_BASE_URL}" ]]; then + echo "external=true" >> "$GITHUB_OUTPUT" + else + echo "external=false" >> "$GITHUB_OUTPUT" + fi + - name: Set up Node - if: steps.gate.outputs.skip != 'true' && secrets.GATEWAY_BASE_URL == '' + if: steps.gate.outputs.skip != 'true' && steps.gw.outputs.external != 'true' uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24 cache: npm - name: Start in-job gateway (free models, no login needed) - if: steps.gate.outputs.skip != 'true' && secrets.GATEWAY_BASE_URL == '' + if: steps.gate.outputs.skip != 'true' && steps.gw.outputs.external != 'true' shell: bash run: | set -euo pipefail diff --git a/.github/workflows/ai-scan.yml b/.github/workflows/ai-scan.yml index bbba7c7..c88ab15 100644 --- a/.github/workflows/ai-scan.yml +++ b/.github/workflows/ai-scan.yml @@ -59,15 +59,29 @@ jobs: echo "::warning::No CPA credentials; CPA fallback channel disabled (gateway only)." fi + - name: Detect gateway source + id: gw + shell: bash + env: + GATEWAY_BASE_URL: ${{ secrets.GATEWAY_BASE_URL }} + run: | + set -euo pipefail + # secrets context is not allowed in step `if:`; expose via outputs. + if [[ -n "${GATEWAY_BASE_URL}" ]]; then + echo "external=true" >> "$GITHUB_OUTPUT" + else + echo "external=false" >> "$GITHUB_OUTPUT" + fi + - name: Set up Node - if: secrets.GATEWAY_BASE_URL == '' + if: steps.gw.outputs.external != 'true' uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24 cache: npm - name: Start in-job gateway (free models, no login needed) - if: secrets.GATEWAY_BASE_URL == '' + if: steps.gw.outputs.external != 'true' shell: bash run: | set -euo pipefail diff --git a/github_bot/src/agent_orchestrator.py b/github_bot/src/agent_orchestrator.py index 1f0326a..49ed49a 100644 --- a/github_bot/src/agent_orchestrator.py +++ b/github_bot/src/agent_orchestrator.py @@ -414,11 +414,13 @@ def run_multi_agent_review(self, ctx: ReviewContext) -> str: "response_ids": [metas[r].get("response_id", "n/a") for r in pipeline if r in metas], "roles": {r: metas.get(r, {}) for r in pipeline}, } - # Only the VERDICT: line counts. Prose mentions of NEEDS_CHANGES must - # not flip the outcome; unparseable outputs degrade to COMMENT. + # Only the VERDICT: line counts (tolerating markdown bold like + # **VERDICT: APPROVE**). Prose mentions of NEEDS_CHANGES must not flip + # the outcome; unparseable outputs degrade to COMMENT. verdict = "APPROVE" for text in results.values(): - m = re.search(r"(?m)^\s*VERDICT\s*:\s*(APPROVE|NEEDS_CHANGES|COMMENT)", text.upper()) + m = re.search(r"(?m)^\s*\*{0,3}\s*VERDICT\s*:\s*(APPROVE|NEEDS_CHANGES|COMMENT)\b", + text.upper()) parsed = m.group(1) if m else "COMMENT" if parsed == "NEEDS_CHANGES": verdict = "NEEDS_CHANGES" diff --git a/tests/test_ai_bot.py b/tests/test_ai_bot.py index 54f2110..6c3a8bf 100644 --- a/tests/test_ai_bot.py +++ b/tests/test_ai_bot.py @@ -14,6 +14,7 @@ from agent_orchestrator import ( # noqa: E402 AgentOrchestrator, + ReviewContext, is_triage_report_publishable, scrub_internal_names, validate_triage_report, @@ -284,8 +285,7 @@ def test_verdict_aggregation_verdict_line_only(self): orch = AgentOrchestrator.__new__(AgentOrchestrator) orch.config = load_json(BOT_DIR / "config" / "bot_config.json") ctx = AgentOrchestrator.__new__(AgentOrchestrator) - import agent_orchestrator as ao - review_ctx = ao.ReviewContext(title="t", body="b") + review_ctx = ReviewContext(title="t", body="b") # Prose mention of NEEDS_CHANGES must not flip an APPROVE verdict. orch._run_role = lambda role, prompt, **kw: ( "VERDICT: APPROVE\nAll good, no NEEDS_CHANGES here.", {"response_id": "r1"}) @@ -306,6 +306,21 @@ def mixed(role, prompt, **kw): report3 = AgentOrchestrator.run_multi_agent_review(orch, review_ctx) self.assertIn("FINAL_VERDICT: NEEDS_CHANGES", report3) + def test_verdict_aggregation_bold_markdown(self): + # Live models wrap the verdict in bold: **VERDICT: APPROVE**. + # Bold-italic (***) is tolerated too. + orch = AgentOrchestrator.__new__(AgentOrchestrator) + orch.config = load_json(BOT_DIR / "config" / "bot_config.json") + review_ctx = ReviewContext(title="t", body="b") + orch._run_role = lambda role, prompt, **kw: ( + "**VERDICT: APPROVE**\n\nAll good.", {"response_id": "r1"}) + report = AgentOrchestrator.run_multi_agent_review(orch, review_ctx) + self.assertIn("FINAL_VERDICT: APPROVE", report) + orch._run_role = lambda role, prompt, **kw: ( + "***VERDICT: APPROVE***\n\nAll good.", {"response_id": "r2"}) + report2 = AgentOrchestrator.run_multi_agent_review(orch, review_ctx) + self.assertIn("FINAL_VERDICT: APPROVE", report2) + class TestScanAndRunner(unittest.TestCase): def test_fingerprint_stable(self):