@agent-native/core 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/README.md +1 -0
  2. package/dist/action.d.ts +8 -1
  3. package/dist/action.d.ts.map +1 -1
  4. package/dist/action.js +20 -10
  5. package/dist/action.js.map +1 -1
  6. package/dist/cli/app-skill.d.ts +3 -1
  7. package/dist/cli/app-skill.d.ts.map +1 -1
  8. package/dist/cli/app-skill.js +50 -8
  9. package/dist/cli/app-skill.js.map +1 -1
  10. package/dist/cli/connect.d.ts.map +1 -1
  11. package/dist/cli/connect.js +39 -5
  12. package/dist/cli/connect.js.map +1 -1
  13. package/dist/cli/create.d.ts.map +1 -1
  14. package/dist/cli/create.js +9 -7
  15. package/dist/cli/create.js.map +1 -1
  16. package/dist/cli/index.js +42 -10
  17. package/dist/cli/index.js.map +1 -1
  18. package/dist/cli/mcp-config-writers.d.ts +10 -0
  19. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  20. package/dist/cli/mcp-config-writers.js +60 -6
  21. package/dist/cli/mcp-config-writers.js.map +1 -1
  22. package/dist/cli/mcp.d.ts.map +1 -1
  23. package/dist/cli/mcp.js +4 -6
  24. package/dist/cli/mcp.js.map +1 -1
  25. package/dist/cli/plan-local.d.ts.map +1 -1
  26. package/dist/cli/plan-local.js +15 -2
  27. package/dist/cli/plan-local.js.map +1 -1
  28. package/dist/cli/plan-publish-store.d.ts +17 -7
  29. package/dist/cli/plan-publish-store.d.ts.map +1 -1
  30. package/dist/cli/plan-publish-store.js +33 -8
  31. package/dist/cli/plan-publish-store.js.map +1 -1
  32. package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
  33. package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
  34. package/dist/cli/pr-visual-recap-workflow.js +1 -1
  35. package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
  36. package/dist/cli/recap.d.ts +63 -5
  37. package/dist/cli/recap.d.ts.map +1 -1
  38. package/dist/cli/recap.js +641 -48
  39. package/dist/cli/recap.js.map +1 -1
  40. package/dist/cli/skills.d.ts +26 -11
  41. package/dist/cli/skills.d.ts.map +1 -1
  42. package/dist/cli/skills.js +644 -972
  43. package/dist/cli/skills.js.map +1 -1
  44. package/dist/cli/templates-meta.d.ts.map +1 -1
  45. package/dist/cli/templates-meta.js +3 -2
  46. package/dist/cli/templates-meta.js.map +1 -1
  47. package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
  48. package/dist/client/blocks/library/AnnotatedCodeBlock.js +37 -9
  49. package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
  50. package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
  51. package/dist/client/blocks/library/DiffBlock.js +44 -12
  52. package/dist/client/blocks/library/DiffBlock.js.map +1 -1
  53. package/dist/client/blocks/library/annotation-rail.d.ts +12 -3
  54. package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -1
  55. package/dist/client/blocks/library/annotation-rail.js +29 -3
  56. package/dist/client/blocks/library/annotation-rail.js.map +1 -1
  57. package/dist/client/blocks/library/html.d.ts.map +1 -1
  58. package/dist/client/blocks/library/html.js +3 -1
  59. package/dist/client/blocks/library/html.js.map +1 -1
  60. package/dist/client/blocks/library/question-form.d.ts.map +1 -1
  61. package/dist/client/blocks/library/question-form.js +4 -1
  62. package/dist/client/blocks/library/question-form.js.map +1 -1
  63. package/dist/client/components/LiveCursorOverlay.d.ts +46 -0
  64. package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -0
  65. package/dist/client/components/LiveCursorOverlay.js +137 -0
  66. package/dist/client/components/LiveCursorOverlay.js.map +1 -0
  67. package/dist/client/components/PresenceBar.d.ts +11 -1
  68. package/dist/client/components/PresenceBar.d.ts.map +1 -1
  69. package/dist/client/components/PresenceBar.js +39 -7
  70. package/dist/client/components/PresenceBar.js.map +1 -1
  71. package/dist/client/components/RemoteSelectionRings.d.ts +43 -0
  72. package/dist/client/components/RemoteSelectionRings.d.ts.map +1 -0
  73. package/dist/client/components/RemoteSelectionRings.js +116 -0
  74. package/dist/client/components/RemoteSelectionRings.js.map +1 -0
  75. package/dist/client/index.d.ts +4 -0
  76. package/dist/client/index.d.ts.map +1 -1
  77. package/dist/client/index.js +5 -0
  78. package/dist/client/index.js.map +1 -1
  79. package/dist/collab/awareness.d.ts +25 -0
  80. package/dist/collab/awareness.d.ts.map +1 -1
  81. package/dist/collab/awareness.js +42 -5
  82. package/dist/collab/awareness.js.map +1 -1
  83. package/dist/collab/client.d.ts +19 -1
  84. package/dist/collab/client.d.ts.map +1 -1
  85. package/dist/collab/client.js +362 -57
  86. package/dist/collab/client.js.map +1 -1
  87. package/dist/collab/follow-mode.d.ts +56 -0
  88. package/dist/collab/follow-mode.d.ts.map +1 -0
  89. package/dist/collab/follow-mode.js +54 -0
  90. package/dist/collab/follow-mode.js.map +1 -0
  91. package/dist/collab/index.d.ts +3 -1
  92. package/dist/collab/index.d.ts.map +1 -1
  93. package/dist/collab/index.js +5 -1
  94. package/dist/collab/index.js.map +1 -1
  95. package/dist/collab/presence.d.ts +56 -0
  96. package/dist/collab/presence.d.ts.map +1 -0
  97. package/dist/collab/presence.js +98 -0
  98. package/dist/collab/presence.js.map +1 -0
  99. package/dist/collab/routes.d.ts.map +1 -1
  100. package/dist/collab/routes.js +33 -6
  101. package/dist/collab/routes.js.map +1 -1
  102. package/dist/collab/struct-routes.d.ts.map +1 -1
  103. package/dist/collab/struct-routes.js +24 -4
  104. package/dist/collab/struct-routes.js.map +1 -1
  105. package/dist/collab/ydoc-manager.d.ts +13 -0
  106. package/dist/collab/ydoc-manager.d.ts.map +1 -1
  107. package/dist/collab/ydoc-manager.js +51 -15
  108. package/dist/collab/ydoc-manager.js.map +1 -1
  109. package/dist/db/migrations.d.ts.map +1 -1
  110. package/dist/db/migrations.js +2 -1
  111. package/dist/db/migrations.js.map +1 -1
  112. package/dist/extensions/routes.d.ts +18 -0
  113. package/dist/extensions/routes.d.ts.map +1 -1
  114. package/dist/extensions/routes.js +30 -8
  115. package/dist/extensions/routes.js.map +1 -1
  116. package/dist/oauth-tokens/store.d.ts.map +1 -1
  117. package/dist/oauth-tokens/store.js +42 -5
  118. package/dist/oauth-tokens/store.js.map +1 -1
  119. package/dist/scripts/db/index.d.ts.map +1 -1
  120. package/dist/scripts/db/index.js +1 -0
  121. package/dist/scripts/db/index.js.map +1 -1
  122. package/dist/scripts/db/migrate-encrypt-oauth-tokens.d.ts +28 -0
  123. package/dist/scripts/db/migrate-encrypt-oauth-tokens.d.ts.map +1 -0
  124. package/dist/scripts/db/migrate-encrypt-oauth-tokens.js +164 -0
  125. package/dist/scripts/db/migrate-encrypt-oauth-tokens.js.map +1 -0
  126. package/dist/scripts/db/scoping.d.ts.map +1 -1
  127. package/dist/scripts/db/scoping.js +7 -5
  128. package/dist/scripts/db/scoping.js.map +1 -1
  129. package/dist/secrets/index.d.ts +1 -0
  130. package/dist/secrets/index.d.ts.map +1 -1
  131. package/dist/secrets/index.js +4 -0
  132. package/dist/secrets/index.js.map +1 -1
  133. package/dist/server/collab-plugin.d.ts +6 -0
  134. package/dist/server/collab-plugin.d.ts.map +1 -1
  135. package/dist/server/collab-plugin.js +105 -5
  136. package/dist/server/collab-plugin.js.map +1 -1
  137. package/dist/server/poll-events.d.ts +5 -0
  138. package/dist/server/poll-events.d.ts.map +1 -1
  139. package/dist/server/poll-events.js +27 -4
  140. package/dist/server/poll-events.js.map +1 -1
  141. package/dist/sharing/actions/set-resource-visibility.d.ts.map +1 -1
  142. package/dist/sharing/actions/set-resource-visibility.js +4 -1
  143. package/dist/sharing/actions/set-resource-visibility.js.map +1 -1
  144. package/dist/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
  145. package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
  146. package/dist/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
  147. package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
  148. package/docs/content/plan-plugin.md +21 -6
  149. package/docs/content/pr-visual-recap.md +52 -3
  150. package/docs/content/real-time-collaboration.md +481 -97
  151. package/docs/content/skills-guide.md +13 -0
  152. package/docs/content/template-plan.md +18 -7
  153. package/package.json +5 -1
  154. package/src/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
  155. package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
  156. package/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
  157. package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
@@ -7,5 +7,5 @@
7
7
  * recap.spec.ts fails if these drift. Regenerate from the YAML with the snippet
8
8
  * in recap.spec.ts.
9
9
  */
10
- export declare const PR_VISUAL_RECAP_WORKFLOW_YML = "name: PR Visual Recap\n\n# Visual code review: a coding agent runs the repo's visual-recap skill over the\n# PR diff, publishes a plan, and upserts one sticky comment with a screenshot.\n# Plain `pull_request` (NOT `pull_request_target`) so fork code never sees secrets.\n\non:\n pull_request:\n types: [opened, synchronize, reopened, ready_for_review]\n\npermissions:\n contents: read\n\nconcurrency:\n group: pr-visual-recap-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n\nenv:\n VISUAL_RECAP_AGENT: ${{ vars.VISUAL_RECAP_AGENT || 'claude' }}\n\njobs:\n gate:\n name: Gate\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: read\n pull-requests: read\n outputs:\n run: ${{ steps.decide.outputs.run }}\n agent: ${{ steps.decide.outputs.agent }}\n steps:\n - id: decide\n uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\n env:\n # Presence-only signals \u2014 never expose secret VALUES to the gate.\n HAS_PLAN: ${{ secrets.PLAN_RECAP_TOKEN != '' }}\n HAS_ANTHROPIC: ${{ secrets.ANTHROPIC_API_KEY != '' }}\n HAS_OPENAI: ${{ secrets.OPENAI_API_KEY != '' }}\n AGENT: ${{ env.VISUAL_RECAP_AGENT }}\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\n with:\n script: |\n const pr = context.payload.pull_request;\n const reasons = [];\n\n if (!pr) reasons.push('no pull_request payload');\n if (pr && pr.draft) reasons.push('draft PR');\n\n // Fork PRs run with no secrets, so publishing would fail anyway \u2014 skip.\n const headRepo = pr && pr.head && pr.head.repo && pr.head.repo.full_name;\n if (pr && headRepo && headRepo !== process.env.GITHUB_REPOSITORY) {\n reasons.push(`fork PR (${headRepo})`);\n }\n\n const login = (pr && pr.user && pr.user.login || '').toLowerCase();\n const botAuthors = ['dependabot[bot]', 'dependabot', 'renovate[bot]', 'renovate'];\n if (botAuthors.includes(login)) reasons.push(`bot author (${login})`);\n if (pr && pr.user && pr.user.type === 'Bot') reasons.push('bot author (type=Bot)');\n\n if (process.env.HAS_PLAN !== 'true') reasons.push('PLAN_RECAP_TOKEN not configured');\n\n // Normalize + validate the agent so a mis-cased value can't pass the\n // gate and then match neither agent step below.\n const agent = (process.env.AGENT || 'claude').toLowerCase();\n if (agent !== 'claude' && agent !== 'codex') {\n reasons.push(`unsupported VISUAL_RECAP_AGENT \"${process.env.AGENT}\" (expected \"claude\" or \"codex\")`);\n } else if (agent === 'codex') {\n if (process.env.HAS_OPENAI !== 'true') reasons.push('OPENAI_API_KEY not configured (codex backend)');\n } else {\n if (process.env.HAS_ANTHROPIC !== 'true') reasons.push('ANTHROPIC_API_KEY not configured (claude backend)');\n }\n\n // Validate the model before it reaches the agent CLI.\n const model = process.env.VISUAL_RECAP_MODEL || '';\n if (model && !/^[a-zA-Z0-9._-]{1,80}$/.test(model)) {\n reasons.push(`invalid VISUAL_RECAP_MODEL value (must match [a-zA-Z0-9._-]{1,80})`);\n }\n\n // Self-modifying guard, evaluated in the trusted gate (runs NO\n // PR-checked-out code): skip the ENTIRE job if the PR touches the\n // workflow, skill, local CLI, or any agent config the runner loads,\n // so a PR can't rewrite what runs and exfiltrate secrets.\n if (pr) {\n try {\n const files = await github.paginate(github.rest.pulls.listFiles, {\n owner: context.repo.owner,\n repo: context.repo.repo,\n pull_number: pr.number,\n per_page: 100,\n });\n const isSensitive = (p) =>\n p === '.github/workflows/pr-visual-recap.yml' ||\n /(^|\\/)skills\\/visual-(recap|plan|plans)\\//.test(p) ||\n /(^|\\/)\\.claude\\//.test(p) ||\n /(^|\\/)CLAUDE\\.md$/.test(p) ||\n /(^|\\/)AGENTS\\.md$/.test(p) ||\n /(^|\\/)\\.mcp\\.json$/.test(p) ||\n /(^|\\/)packages\\/core\\//.test(p);\n const hits = files.map((f) => f.filename).filter(isSensitive);\n if (hits.length) {\n reasons.push(`PR modifies recap-control files (${hits.slice(0, 3).join(', ')}${hits.length > 3 ? ', \u2026' : ''}) \u2014 skipping so untrusted PR code never runs with secrets`);\n }\n } catch (e) {\n // Fail closed: if the file list can't be read, skip.\n reasons.push(`could not list PR files for the self-modifying guard (${e.message}); skipping to be safe`);\n }\n }\n\n const run = reasons.length === 0;\n core.setOutput('run', run ? 'true' : 'false');\n core.setOutput('agent', agent);\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join('; ')}`);\n\n recap:\n name: Generate visual recap\n needs: gate\n if: needs.gate.outputs.run == 'true'\n runs-on: ubuntu-latest\n timeout-minutes: 30\n permissions:\n checks: write\n contents: read\n issues: write\n pull-requests: write\n env:\n PLAN_RECAP_APP_URL: ${{ secrets.PLAN_RECAP_APP_URL || 'https://plan.agent-native.com' }}\n PLAN_RECAP_TOKEN: ${{ secrets.PLAN_RECAP_TOKEN }}\n GH_TOKEN: ${{ github.token }}\n PR_NUMBER: ${{ github.event.pull_request.number }}\n HEAD_SHA: ${{ github.event.pull_request.head.sha }}\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\n VISUAL_RECAP_REASONING: ${{ vars.VISUAL_RECAP_REASONING }}\n steps:\n - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n with:\n fetch-depth: 0\n # This job runs an agent over untrusted PR diff; don't leave the token\n # in .git/config (it uses GH_TOKEN for gh API calls, never git push).\n persist-credentials: false\n\n # Dogfood local source inside this monorepo, else the published package.\n # The pnpm steps run ONLY on the local path so npm/yarn consumer repos\n # (no pnpm-lock.yaml) fall back to `npx @agent-native/core`.\n - name: Resolve recap CLI\n id: cli\n run: |\n if [ -f packages/core/src/cli/index.ts ]; then\n echo \"RECAP_CLI=pnpm exec tsx packages/core/src/cli/index.ts\" >> \"$GITHUB_ENV\"\n echo \"local=true\" >> \"$GITHUB_OUTPUT\"\n else\n echo \"RECAP_CLI=npx -y @agent-native/core@latest\" >> \"$GITHUB_ENV\"\n echo \"local=false\" >> \"$GITHUB_OUTPUT\"\n fi\n\n - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8\n if: steps.cli.outputs.local == 'true'\n\n - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version: \"22\"\n cache: ${{ steps.cli.outputs.local == 'true' && 'pnpm' || '' }}\n\n - name: Install workspace (local source only)\n if: steps.cli.outputs.local == 'true'\n run: pnpm install --frozen-lockfile --ignore-scripts\n\n - name: Start visual recap check\n id: recap_check\n continue-on-error: true\n run: |\n set -uo pipefail\n $RECAP_CLI recap check start --sha \"$HEAD_SHA\" --workflow-url \"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID\"\n\n - name: Collect bounded diff\n id: diff\n env:\n BASE_SHA: ${{ github.event.pull_request.base.sha }}\n run: |\n set -euo pipefail\n $RECAP_CLI recap collect-diff --base \"$BASE_SHA\" --head \"$HEAD_SHA\" --out recap.diff --stat recap.stat\n\n - name: Secret scan\n id: scan\n if: steps.diff.outputs.tiny != 'true'\n run: |\n set -uo pipefail\n # Fail CLOSED: a scanner error or invalid JSON suppresses the diff so a\n # credential-bearing diff is never handed to the agent / plan service.\n if ! SCAN_JSON=\"$($RECAP_CLI recap scan --diff recap.diff)\"; then\n SCAN_JSON='{\"suppressed\":true,\"reason\":\"secret scan failed to run; failing closed\"}'\n fi\n {\n echo 'json<<__RECAP_SCAN_EOF__'\n echo \"$SCAN_JSON\"\n echo '__RECAP_SCAN_EOF__'\n } >> \"$GITHUB_OUTPUT\"\n SUPPRESSED=$(node -e 'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?\"true\":\"false\")}catch{process.stdout.write(\"true\")}' \"$SCAN_JSON\")\n echo \"suppressed=$SUPPRESSED\" >> \"$GITHUB_OUTPUT\"\n\n - name: Read previous plan id\n id: prev\n continue-on-error: true\n run: |\n set -euo pipefail\n PLAN_ID=\"$($RECAP_CLI recap comment find-plan-id --repo \"$GITHUB_REPOSITORY\" --issue \"$PR_NUMBER\" --token \"$GH_TOKEN\")\"\n echo \"plan_id=$PLAN_ID\" >> \"$GITHUB_OUTPUT\"\n\n - name: Build recap prompt\n id: prompt\n if: steps.diff.outputs.tiny != 'true' && steps.scan.outputs.suppressed != 'true'\n env:\n # Pass step outputs via env, NOT ${{ }} interpolation into the run body:\n # the prev plan id is parsed from a PR comment and could inject shell.\n PREV_PLAN_ID: ${{ steps.prev.outputs.plan_id }}\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\n run: |\n set -euo pipefail\n ARGS=(--diff recap.diff --stat recap.stat --pr \"$PR_NUMBER\" --repo \"$GITHUB_REPOSITORY\" --head \"$HEAD_SHA\" --app-url \"$PLAN_RECAP_APP_URL\" --out recap-prompt.md)\n if [ \"${DIFF_HUGE:-}\" = \"true\" ]; then ARGS+=(--huge); fi\n if [ -n \"${PREV_PLAN_ID:-}\" ]; then ARGS+=(--prev-plan-id \"$PREV_PLAN_ID\"); fi\n $RECAP_CLI recap build-prompt \"${ARGS[@]}\"\n\n - name: Run agent (Claude Code)\n id: claude\n if: needs.gate.outputs.agent == 'claude' && steps.diff.outputs.tiny != 'true' && steps.scan.outputs.suppressed != 'true'\n continue-on-error: true\n env:\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n run: |\n set -uo pipefail\n MCP_CONFIG=\"$RUNNER_TEMP/plan-mcp.json\"\n $RECAP_CLI recap mcp-config --agent claude --app-url \"$PLAN_RECAP_APP_URL\" --out \"$MCP_CONFIG\"\n CLAUDE_ARGS=(-p \"$(cat recap-prompt.md)\" --mcp-config \"$MCP_CONFIG\" --allowedTools \"Read,Write,Bash(git diff:*),mcp__plan__get-plan-blocks,mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility\" --permission-mode dontAsk --output-format json)\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CLAUDE_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\n npx -y @anthropic-ai/claude-code@2 \"${CLAUDE_ARGS[@]}\" > claude-result.json || true\n rm -f \"$MCP_CONFIG\" || true\n\n - name: Run agent (Codex)\n id: codex\n if: needs.gate.outputs.agent == 'codex' && steps.diff.outputs.tiny != 'true' && steps.scan.outputs.suppressed != 'true'\n continue-on-error: true\n env:\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n run: |\n set -uo pipefail\n $RECAP_CLI recap mcp-config --agent codex --app-url \"$PLAN_RECAP_APP_URL\"\n # `codex login` writes ~/.codex/auth.json (the bare env var is dropped on\n # the gpt-5.5 wss transport); stdin keeps the key out of process args.\n printenv OPENAI_API_KEY | npx -y @openai/codex@0 login --with-api-key || true\n # The runner is itself an ephemeral sandbox; bypass Codex's own sandbox\n # (bubblewrap can't init here) and approval gate (cancels the MCP write).\n CODEX_ARGS=(exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check)\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CODEX_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\n # Validate reasoning against the enum before embedding it in the TOML override.\n case \"${VISUAL_RECAP_REASONING:-}\" in\n none|minimal|low|medium|high|xhigh)\n CODEX_ARGS+=(-c \"model_reasoning_effort=\\\"$VISUAL_RECAP_REASONING\\\"\") ;;\n \"\") ;;\n *) echo \"Ignoring invalid VISUAL_RECAP_REASONING: $VISUAL_RECAP_REASONING\" ;;\n esac\n npx -y @openai/codex@0 \"${CODEX_ARGS[@]}\" --json \"$(cat recap-prompt.md)\" | tee codex-events.jsonl || true\n\n - name: Read plan URL\n id: url\n if: steps.diff.outputs.tiny != 'true' && steps.scan.outputs.suppressed != 'true'\n run: |\n set -uo pipefail\n PLAN_URL=\"\"\n if [ -f recap-url.txt ]; then PLAN_URL=\"$(tr -d '\\r\\n' < recap-url.txt | tr -d ' ')\"; fi\n # recap-url.txt is agent-written \u2192 untrusted. Only proceed if its origin\n # matches the trusted plan app, so a prompt-injected URL can't drive the\n # screenshot/comment steps. Mismatch or empty \u2192 ok=false.\n OK=$(PLAN_URL=\"$PLAN_URL\" node -e 'try{const u=new URL(process.env.PLAN_URL||\"\");const t=new URL(process.env.PLAN_RECAP_APP_URL||\"https://plan.agent-native.com\");process.stdout.write(u.origin===t.origin?\"true\":\"false\")}catch{process.stdout.write(\"false\")}')\n if [ \"$OK\" = \"true\" ]; then\n echo \"plan_url=$PLAN_URL\" >> \"$GITHUB_OUTPUT\"; echo \"ok=true\" >> \"$GITHUB_OUTPUT\"\n else\n echo \"plan_url=\" >> \"$GITHUB_OUTPUT\"; echo \"ok=false\" >> \"$GITHUB_OUTPUT\"\n fi\n\n - name: Attach usage\n if: steps.url.outputs.ok == 'true'\n continue-on-error: true\n env:\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n # Use the gate-normalized agent so \"Codex\" still selects the right file.\n RECAP_AGENT: ${{ needs.gate.outputs.agent }}\n run: |\n set -uo pipefail\n RESULT=claude-result.json\n if [ \"$RECAP_AGENT\" = \"codex\" ]; then RESULT=codex-events.jsonl; fi\n if [ -f \"$RESULT\" ]; then $RECAP_CLI recap usage --plan-url \"$PLAN_URL\" --agent \"$RECAP_AGENT\" --result-file \"$RESULT\" --model \"${VISUAL_RECAP_MODEL:-}\" --app-url \"$PLAN_RECAP_APP_URL\" --token \"$PLAN_RECAP_TOKEN\" || true; fi\n\n - name: Screenshot + upload\n id: shot\n if: steps.url.outputs.ok == 'true'\n continue-on-error: true\n env:\n # recap-url.txt is untrusted agent output; pass via env, never ${{ }}.\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n run: |\n set -uo pipefail\n pnpm exec playwright install --with-deps chromium 2>/dev/null || npx -y playwright@1 install --with-deps chromium || true\n SHOT_JSON=\"$($RECAP_CLI recap shot --url \"$PLAN_URL\" --token \"$PLAN_RECAP_TOKEN\" --app-url \"$PLAN_RECAP_APP_URL\" --out recap.png || echo '{}')\"\n IMAGE_URL=$(node -e 'try{process.stdout.write(JSON.parse(process.argv[1]).imageUrl||\"\")}catch{process.stdout.write(\"\")}' \"$SHOT_JSON\")\n echo \"image_url=$IMAGE_URL\" >> \"$GITHUB_OUTPUT\"\n if [ -f recap.png ]; then echo \"captured=true\" >> \"$GITHUB_OUTPUT\"; else echo \"captured=false\" >> \"$GITHUB_OUTPUT\"; fi\n\n - name: Upload recap screenshot artifact\n if: steps.shot.outputs.captured == 'true'\n uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n with:\n name: pr-visual-recap-${{ github.event.pull_request.number }}\n path: recap.png\n if-no-files-found: ignore\n retention-days: 14\n\n - name: Upsert sticky comment\n if: always()\n continue-on-error: true\n env:\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n RECAP_IMAGE_URL: ${{ steps.shot.outputs.image_url }}\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\n run: |\n set -euo pipefail\n ARGS=(recap comment upsert --repo \"$GITHUB_REPOSITORY\" --issue \"$PR_NUMBER\" --token \"$GH_TOKEN\")\n # On a tiny diff, only REFRESH an existing comment, never create one.\n if [ \"${DIFF_TINY:-}\" = \"true\" ]; then ARGS+=(--update-only); fi\n $RECAP_CLI \"${ARGS[@]}\"\n\n - name: Complete visual recap check\n if: always() && steps.recap_check.outputs.check_run_id != ''\n continue-on-error: true\n env:\n # Untrusted/step values via env (NOT ${{ }}-interpolated into the run\n # body): the agent-written plan URL and the scan JSON could inject shell.\n CHECK_RUN_ID: ${{ steps.recap_check.outputs.check_run_id }}\n PLAN_OK: ${{ steps.url.outputs.ok }}\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\n run: |\n set -uo pipefail\n $RECAP_CLI recap check complete \\\n --check-run-id \"$CHECK_RUN_ID\" \\\n --plan-ok \"$PLAN_OK\" \\\n --plan-url \"$PLAN_URL\" \\\n --suppressed \"$SUPPRESSED\" \\\n --suppressed-json \"$SUPPRESSED_JSON\" \\\n --huge \"$DIFF_HUGE\" \\\n --tiny \"$DIFF_TINY\" \\\n --workflow-url \"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID\"\n";
10
+ export declare const PR_VISUAL_RECAP_WORKFLOW_YML = "name: PR Visual Recap\n\n# Visual code review: a coding agent runs the repo's visual-recap skill over the\n# PR diff, publishes a plan, and upserts one sticky comment with a screenshot.\n# Plain `pull_request` (NOT `pull_request_target`) so fork code never sees secrets.\n\non:\n pull_request:\n types: [opened, synchronize, reopened, ready_for_review]\n\npermissions:\n contents: read\n\nconcurrency:\n group: pr-visual-recap-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n\nenv:\n VISUAL_RECAP_AGENT: ${{ vars.VISUAL_RECAP_AGENT || 'claude' }}\n VISUAL_RECAP_SKILL_SOURCE: ${{ vars.VISUAL_RECAP_SKILL_SOURCE || 'auto' }}\n\njobs:\n gate:\n name: Gate\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: read\n pull-requests: read\n outputs:\n run: ${{ steps.decide.outputs.run }}\n agent: ${{ steps.decide.outputs.agent }}\n steps:\n - id: decide\n uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\n env:\n # Presence-only signals \u2014 never expose secret VALUES to the gate.\n HAS_PLAN: ${{ secrets.PLAN_RECAP_TOKEN != '' }}\n HAS_ANTHROPIC: ${{ secrets.ANTHROPIC_API_KEY != '' }}\n HAS_OPENAI: ${{ secrets.OPENAI_API_KEY != '' }}\n AGENT: ${{ env.VISUAL_RECAP_AGENT }}\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\n with:\n script: |\n const pr = context.payload.pull_request;\n const reasons = [];\n\n if (!pr) reasons.push('no pull_request payload');\n if (pr && pr.draft) reasons.push('draft PR');\n\n // Fork PRs run with no secrets, so publishing would fail anyway \u2014 skip.\n const headRepo = pr && pr.head && pr.head.repo && pr.head.repo.full_name;\n if (pr && headRepo && headRepo !== process.env.GITHUB_REPOSITORY) {\n reasons.push(`fork PR (${headRepo})`);\n }\n\n const login = (pr && pr.user && pr.user.login || '').toLowerCase();\n const botAuthors = ['dependabot[bot]', 'dependabot', 'renovate[bot]', 'renovate'];\n if (botAuthors.includes(login)) reasons.push(`bot author (${login})`);\n if (pr && pr.user && pr.user.type === 'Bot') reasons.push('bot author (type=Bot)');\n\n if (process.env.HAS_PLAN !== 'true') reasons.push('PLAN_RECAP_TOKEN not configured');\n\n // Normalize + validate the agent so a mis-cased value can't pass the\n // gate and then match neither agent step below.\n const agent = (process.env.AGENT || 'claude').toLowerCase();\n if (agent !== 'claude' && agent !== 'codex') {\n reasons.push(`unsupported VISUAL_RECAP_AGENT \"${process.env.AGENT}\" (expected \"claude\" or \"codex\")`);\n } else if (agent === 'codex') {\n if (process.env.HAS_OPENAI !== 'true') reasons.push('OPENAI_API_KEY not configured (codex backend)');\n } else {\n if (process.env.HAS_ANTHROPIC !== 'true') reasons.push('ANTHROPIC_API_KEY not configured (claude backend)');\n }\n\n // Validate the model before it reaches the agent CLI.\n const model = process.env.VISUAL_RECAP_MODEL || '';\n if (model && !/^[a-zA-Z0-9._-]{1,80}$/.test(model)) {\n reasons.push(`invalid VISUAL_RECAP_MODEL value (must match [a-zA-Z0-9._-]{1,80})`);\n }\n\n // Self-modifying guard, evaluated in the trusted gate (runs NO\n // PR-checked-out code): skip the ENTIRE job if the PR touches the\n // workflow, skill, local CLI, or any agent config the runner loads,\n // so a PR can't rewrite what runs and exfiltrate secrets.\n if (pr) {\n try {\n const files = await github.paginate(github.rest.pulls.listFiles, {\n owner: context.repo.owner,\n repo: context.repo.repo,\n pull_number: pr.number,\n per_page: 100,\n });\n const isAgentNativeMonorepo = context.repo.owner === 'BuilderIO' && context.repo.repo === 'agent-native';\n const isSensitive = (p) =>\n p === '.github/workflows/pr-visual-recap.yml' ||\n /(^|\\/)skills\\/visual-(recap|plan|plans)\\//.test(p) ||\n /(^|\\/)\\.claude\\//.test(p) ||\n /(^|\\/)CLAUDE\\.md$/.test(p) ||\n /(^|\\/)AGENTS\\.md$/.test(p) ||\n /(^|\\/)\\.mcp\\.json$/.test(p) ||\n (isAgentNativeMonorepo && /(^|\\/)packages\\/core\\//.test(p));\n const hits = files.map((f) => f.filename).filter(isSensitive);\n if (hits.length) {\n reasons.push(`PR modifies recap-control files (${hits.slice(0, 3).join(', ')}${hits.length > 3 ? ', \u2026' : ''}) \u2014 skipping so untrusted PR code never runs with secrets`);\n }\n } catch (e) {\n // Fail closed: if the file list can't be read, skip.\n reasons.push(`could not list PR files for the self-modifying guard (${e.message}); skipping to be safe`);\n }\n }\n\n const run = reasons.length === 0;\n core.setOutput('run', run ? 'true' : 'false');\n core.setOutput('agent', agent);\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join('; ')}`);\n\n recap:\n name: Generate visual recap\n needs: gate\n if: needs.gate.outputs.run == 'true'\n runs-on: ubuntu-latest\n timeout-minutes: 30\n permissions:\n checks: write\n contents: read\n issues: write\n pull-requests: write\n env:\n PLAN_RECAP_APP_URL: ${{ secrets.PLAN_RECAP_APP_URL || 'https://plan.agent-native.com' }}\n PLAN_RECAP_TOKEN: ${{ secrets.PLAN_RECAP_TOKEN }}\n GH_TOKEN: ${{ github.token }}\n PR_NUMBER: ${{ github.event.pull_request.number }}\n HEAD_SHA: ${{ github.event.pull_request.head.sha }}\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\n VISUAL_RECAP_REASONING: ${{ vars.VISUAL_RECAP_REASONING }}\n VISUAL_RECAP_SKILL_SOURCE: ${{ vars.VISUAL_RECAP_SKILL_SOURCE || 'auto' }}\n steps:\n - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n with:\n fetch-depth: 0\n # This job runs an agent over untrusted PR diff; don't leave the token\n # in .git/config (it uses GH_TOKEN for gh API calls, never git push).\n persist-credentials: false\n\n # Dogfood local source inside this monorepo, else the published package.\n # The pnpm steps run ONLY on the local path so npm/yarn consumer repos\n # (no pnpm-lock.yaml) fall back to `npx @agent-native/core`.\n - name: Resolve recap CLI\n id: cli\n run: |\n if [ -f packages/core/src/cli/index.ts ]; then\n echo \"RECAP_CLI=pnpm exec tsx packages/core/src/cli/index.ts\" >> \"$GITHUB_ENV\"\n echo \"local=true\" >> \"$GITHUB_OUTPUT\"\n else\n echo \"RECAP_CLI=npx -y @agent-native/core@latest\" >> \"$GITHUB_ENV\"\n echo \"local=false\" >> \"$GITHUB_OUTPUT\"\n fi\n\n - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8\n if: steps.cli.outputs.local == 'true'\n\n - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version: \"22\"\n cache: ${{ steps.cli.outputs.local == 'true' && 'pnpm' || '' }}\n\n - name: Install workspace (local source only)\n if: steps.cli.outputs.local == 'true'\n run: pnpm install --frozen-lockfile --ignore-scripts\n\n - name: Start visual recap check\n id: recap_check\n continue-on-error: true\n run: |\n set -uo pipefail\n $RECAP_CLI recap check start --sha \"$HEAD_SHA\" --workflow-url \"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID\"\n\n - name: Collect bounded diff\n id: diff\n env:\n BASE_SHA: ${{ github.event.pull_request.base.sha }}\n run: |\n set -euo pipefail\n $RECAP_CLI recap collect-diff --base \"$BASE_SHA\" --head \"$HEAD_SHA\" --out recap.diff --stat recap.stat\n\n - name: Secret scan\n id: scan\n if: steps.diff.outputs.tiny != 'true'\n run: |\n set -uo pipefail\n # Fail CLOSED: a scanner error or invalid JSON suppresses the diff so a\n # credential-bearing diff is never handed to the agent / plan service.\n if ! SCAN_JSON=\"$($RECAP_CLI recap scan --diff recap.diff)\"; then\n SCAN_JSON='{\"suppressed\":true,\"reason\":\"secret scan failed to run; failing closed\"}'\n fi\n {\n echo 'json<<__RECAP_SCAN_EOF__'\n echo \"$SCAN_JSON\"\n echo '__RECAP_SCAN_EOF__'\n } >> \"$GITHUB_OUTPUT\"\n SUPPRESSED=$(node -e 'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?\"true\":\"false\")}catch{process.stdout.write(\"true\")}' \"$SCAN_JSON\")\n echo \"suppressed=$SUPPRESSED\" >> \"$GITHUB_OUTPUT\"\n\n - name: Read previous plan id\n id: prev\n continue-on-error: true\n run: |\n set -euo pipefail\n PLAN_ID=\"$($RECAP_CLI recap comment find-plan-id --repo \"$GITHUB_REPOSITORY\" --issue \"$PR_NUMBER\" --token \"$GH_TOKEN\")\"\n echo \"plan_id=$PLAN_ID\" >> \"$GITHUB_OUTPUT\"\n\n - name: Build recap prompt\n id: prompt\n if: steps.diff.outputs.tiny != 'true' && steps.scan.outputs.suppressed != 'true'\n env:\n # Pass step outputs via env, NOT ${{ }} interpolation into the run body:\n # the prev plan id is parsed from a PR comment and could inject shell.\n PREV_PLAN_ID: ${{ steps.prev.outputs.plan_id }}\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\n run: |\n set -euo pipefail\n ARGS=(--diff recap.diff --stat recap.stat --pr \"$PR_NUMBER\" --repo \"$GITHUB_REPOSITORY\" --head \"$HEAD_SHA\" --app-url \"$PLAN_RECAP_APP_URL\" --skill-source \"$VISUAL_RECAP_SKILL_SOURCE\" --out recap-prompt.md)\n if [ \"${DIFF_HUGE:-}\" = \"true\" ]; then ARGS+=(--huge); fi\n if [ -n \"${PREV_PLAN_ID:-}\" ]; then ARGS+=(--prev-plan-id \"$PREV_PLAN_ID\"); fi\n $RECAP_CLI recap build-prompt \"${ARGS[@]}\"\n\n - name: Run agent (Claude Code)\n id: claude\n if: needs.gate.outputs.agent == 'claude' && steps.diff.outputs.tiny != 'true' && steps.scan.outputs.suppressed != 'true'\n continue-on-error: true\n env:\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n run: |\n set -uo pipefail\n MCP_CONFIG=\"$RUNNER_TEMP/plan-mcp.json\"\n $RECAP_CLI recap mcp-config --agent claude --app-url \"$PLAN_RECAP_APP_URL\" --out \"$MCP_CONFIG\"\n CLAUDE_ARGS=(-p \"$(cat recap-prompt.md)\" --mcp-config \"$MCP_CONFIG\" --allowedTools \"Read,Write,Bash(git diff:*),mcp__plan__get-plan-blocks,mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility\" --permission-mode dontAsk --output-format json)\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CLAUDE_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\n npx -y @anthropic-ai/claude-code@2 \"${CLAUDE_ARGS[@]}\" > claude-result.json || true\n rm -f \"$MCP_CONFIG\" || true\n\n - name: Run agent (Codex)\n id: codex\n if: needs.gate.outputs.agent == 'codex' && steps.diff.outputs.tiny != 'true' && steps.scan.outputs.suppressed != 'true'\n continue-on-error: true\n env:\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n run: |\n set -uo pipefail\n $RECAP_CLI recap mcp-config --agent codex --app-url \"$PLAN_RECAP_APP_URL\"\n # `codex login` writes ~/.codex/auth.json (the bare env var is dropped on\n # the gpt-5.5 wss transport); stdin keeps the key out of process args.\n printenv OPENAI_API_KEY | npx -y @openai/codex@0 login --with-api-key || true\n # The runner is itself an ephemeral sandbox; bypass Codex's own sandbox\n # (bubblewrap can't init here) and approval gate (cancels the MCP write).\n CODEX_ARGS=(exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check)\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CODEX_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\n # Validate reasoning against the enum before embedding it in the TOML override.\n case \"${VISUAL_RECAP_REASONING:-}\" in\n none|minimal|low|medium|high|xhigh)\n CODEX_ARGS+=(-c \"model_reasoning_effort=\\\"$VISUAL_RECAP_REASONING\\\"\") ;;\n \"\") ;;\n *) echo \"Ignoring invalid VISUAL_RECAP_REASONING: $VISUAL_RECAP_REASONING\" ;;\n esac\n npx -y @openai/codex@0 \"${CODEX_ARGS[@]}\" --json \"$(cat recap-prompt.md)\" | tee codex-events.jsonl || true\n\n - name: Read plan URL\n id: url\n if: steps.diff.outputs.tiny != 'true' && steps.scan.outputs.suppressed != 'true'\n run: |\n set -uo pipefail\n PLAN_URL=\"\"\n if [ -f recap-url.txt ]; then PLAN_URL=\"$(tr -d '\\r\\n' < recap-url.txt | tr -d ' ')\"; fi\n # recap-url.txt is agent-written -> untrusted. Rebuild a canonical\n # recap URL from the trusted app base and a strictly validated plan id,\n # preserving path-prefixed self-hosted mounts.\n CANONICAL_URL=$(PLAN_URL=\"$PLAN_URL\" node <<'NODE'\n try {\n const raw = process.env.PLAN_URL || \"\";\n const trusted = new URL(process.env.PLAN_RECAP_APP_URL || \"https://plan.agent-native.com\");\n const parsed = /^https?:\\/\\//i.test(raw)\n ? new URL(raw)\n : new URL(raw, trusted);\n if (parsed.origin !== trusted.origin) {\n process.exit(0);\n }\n\n const base = trusted.pathname.replace(/\\/$/, \"\");\n const paths = [parsed.pathname];\n if (base && parsed.pathname.startsWith(`${base}/`)) {\n paths.push(parsed.pathname.slice(base.length) || \"/\");\n }\n\n for (const path of paths) {\n const match = path.match(/^\\/(?:plans|recaps)\\/([A-Za-z0-9_-]+)\\/?$/);\n if (match) {\n process.stdout.write(`${trusted.origin}${base}/recaps/${match[1]}`);\n break;\n }\n }\n } catch {\n process.exit(0);\n }\n NODE\n )\n if [ -n \"$CANONICAL_URL\" ]; then\n echo \"plan_url=$CANONICAL_URL\" >> \"$GITHUB_OUTPUT\"; echo \"ok=true\" >> \"$GITHUB_OUTPUT\"\n else\n echo \"plan_url=\" >> \"$GITHUB_OUTPUT\"; echo \"ok=false\" >> \"$GITHUB_OUTPUT\"\n fi\n\n - name: Attach usage\n if: steps.url.outputs.ok == 'true'\n continue-on-error: true\n env:\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n # Use the gate-normalized agent so \"Codex\" still selects the right file.\n RECAP_AGENT: ${{ needs.gate.outputs.agent }}\n run: |\n set -uo pipefail\n RESULT=claude-result.json\n if [ \"$RECAP_AGENT\" = \"codex\" ]; then RESULT=codex-events.jsonl; fi\n if [ -f \"$RESULT\" ]; then $RECAP_CLI recap usage --plan-url \"$PLAN_URL\" --agent \"$RECAP_AGENT\" --result-file \"$RESULT\" --model \"${VISUAL_RECAP_MODEL:-}\" --app-url \"$PLAN_RECAP_APP_URL\" --token \"$PLAN_RECAP_TOKEN\" || true; fi\n\n - name: Cache Playwright browsers\n if: steps.url.outputs.ok == 'true'\n uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3\n with:\n path: ~/.cache/ms-playwright\n key: playwright-1-${{ runner.os }}\n\n - name: Screenshot + upload\n id: shot\n if: steps.url.outputs.ok == 'true'\n continue-on-error: true\n env:\n # recap-url.txt is untrusted agent output; pass via env, never ${{ }}.\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n run: |\n set -uo pipefail\n pnpm exec playwright install --with-deps chromium 2>/dev/null || npx -y playwright@1 install --with-deps chromium || true\n SHOT_JSON=\"$($RECAP_CLI recap shot --url \"$PLAN_URL\" --token \"$PLAN_RECAP_TOKEN\" --app-url \"$PLAN_RECAP_APP_URL\" --out recap.png || echo '{}')\"\n IMAGE_URL=$(node -e 'try{process.stdout.write(JSON.parse(process.argv[1]).imageUrl||\"\")}catch{process.stdout.write(\"\")}' \"$SHOT_JSON\")\n echo \"image_url=$IMAGE_URL\" >> \"$GITHUB_OUTPUT\"\n if [ -f recap.png ]; then echo \"captured=true\" >> \"$GITHUB_OUTPUT\"; else echo \"captured=false\" >> \"$GITHUB_OUTPUT\"; fi\n\n - name: Upload recap screenshot artifact\n if: steps.shot.outputs.captured == 'true'\n uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n with:\n name: pr-visual-recap-${{ github.event.pull_request.number }}\n path: recap.png\n if-no-files-found: ignore\n retention-days: 14\n\n - name: Upsert sticky comment\n if: always()\n continue-on-error: true\n env:\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n RECAP_IMAGE_URL: ${{ steps.shot.outputs.image_url }}\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\n PREV_PLAN_ID: ${{ steps.prev.outputs.plan_id }}\n run: |\n set -euo pipefail\n ARGS=(recap comment upsert --repo \"$GITHUB_REPOSITORY\" --issue \"$PR_NUMBER\" --token \"$GH_TOKEN\")\n # On a tiny diff, only REFRESH an existing comment, never create one.\n if [ \"${DIFF_TINY:-}\" = \"true\" ]; then ARGS+=(--update-only); fi\n $RECAP_CLI \"${ARGS[@]}\"\n\n - name: Complete visual recap check\n if: always() && steps.recap_check.outputs.check_run_id != ''\n continue-on-error: true\n env:\n # Untrusted/step values via env (NOT ${{ }}-interpolated into the run\n # body): the agent-written plan URL and the scan JSON could inject shell.\n CHECK_RUN_ID: ${{ steps.recap_check.outputs.check_run_id }}\n PLAN_OK: ${{ steps.url.outputs.ok }}\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\n run: |\n set -uo pipefail\n $RECAP_CLI recap check complete \\\n --check-run-id \"$CHECK_RUN_ID\" \\\n --plan-ok \"$PLAN_OK\" \\\n --plan-url \"$PLAN_URL\" \\\n --suppressed \"$SUPPRESSED\" \\\n --suppressed-json \"$SUPPRESSED_JSON\" \\\n --huge \"$DIFF_HUGE\" \\\n --tiny \"$DIFF_TINY\" \\\n --workflow-url \"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID\"\n";
11
11
  //# sourceMappingURL=pr-visual-recap-workflow.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"pr-visual-recap-workflow.d.ts","sourceRoot":"","sources":["../../src/cli/pr-visual-recap-workflow.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,4BAA4B,wsjBAC4hjB,CAAC"}
1
+ {"version":3,"file":"pr-visual-recap-workflow.d.ts","sourceRoot":"","sources":["../../src/cli/pr-visual-recap-workflow.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,4BAA4B,irmBAC0hmB,CAAC"}
@@ -7,5 +7,5 @@
7
7
  * recap.spec.ts fails if these drift. Regenerate from the YAML with the snippet
8
8
  * in recap.spec.ts.
9
9
  */
10
- export const PR_VISUAL_RECAP_WORKFLOW_YML = 'name: PR Visual Recap\n\n# Visual code review: a coding agent runs the repo\'s visual-recap skill over the\n# PR diff, publishes a plan, and upserts one sticky comment with a screenshot.\n# Plain `pull_request` (NOT `pull_request_target`) so fork code never sees secrets.\n\non:\n pull_request:\n types: [opened, synchronize, reopened, ready_for_review]\n\npermissions:\n contents: read\n\nconcurrency:\n group: pr-visual-recap-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n\nenv:\n VISUAL_RECAP_AGENT: ${{ vars.VISUAL_RECAP_AGENT || \'claude\' }}\n\njobs:\n gate:\n name: Gate\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: read\n pull-requests: read\n outputs:\n run: ${{ steps.decide.outputs.run }}\n agent: ${{ steps.decide.outputs.agent }}\n steps:\n - id: decide\n uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\n env:\n # Presence-only signals — never expose secret VALUES to the gate.\n HAS_PLAN: ${{ secrets.PLAN_RECAP_TOKEN != \'\' }}\n HAS_ANTHROPIC: ${{ secrets.ANTHROPIC_API_KEY != \'\' }}\n HAS_OPENAI: ${{ secrets.OPENAI_API_KEY != \'\' }}\n AGENT: ${{ env.VISUAL_RECAP_AGENT }}\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\n with:\n script: |\n const pr = context.payload.pull_request;\n const reasons = [];\n\n if (!pr) reasons.push(\'no pull_request payload\');\n if (pr && pr.draft) reasons.push(\'draft PR\');\n\n // Fork PRs run with no secrets, so publishing would fail anyway — skip.\n const headRepo = pr && pr.head && pr.head.repo && pr.head.repo.full_name;\n if (pr && headRepo && headRepo !== process.env.GITHUB_REPOSITORY) {\n reasons.push(`fork PR (${headRepo})`);\n }\n\n const login = (pr && pr.user && pr.user.login || \'\').toLowerCase();\n const botAuthors = [\'dependabot[bot]\', \'dependabot\', \'renovate[bot]\', \'renovate\'];\n if (botAuthors.includes(login)) reasons.push(`bot author (${login})`);\n if (pr && pr.user && pr.user.type === \'Bot\') reasons.push(\'bot author (type=Bot)\');\n\n if (process.env.HAS_PLAN !== \'true\') reasons.push(\'PLAN_RECAP_TOKEN not configured\');\n\n // Normalize + validate the agent so a mis-cased value can\'t pass the\n // gate and then match neither agent step below.\n const agent = (process.env.AGENT || \'claude\').toLowerCase();\n if (agent !== \'claude\' && agent !== \'codex\') {\n reasons.push(`unsupported VISUAL_RECAP_AGENT "${process.env.AGENT}" (expected "claude" or "codex")`);\n } else if (agent === \'codex\') {\n if (process.env.HAS_OPENAI !== \'true\') reasons.push(\'OPENAI_API_KEY not configured (codex backend)\');\n } else {\n if (process.env.HAS_ANTHROPIC !== \'true\') reasons.push(\'ANTHROPIC_API_KEY not configured (claude backend)\');\n }\n\n // Validate the model before it reaches the agent CLI.\n const model = process.env.VISUAL_RECAP_MODEL || \'\';\n if (model && !/^[a-zA-Z0-9._-]{1,80}$/.test(model)) {\n reasons.push(`invalid VISUAL_RECAP_MODEL value (must match [a-zA-Z0-9._-]{1,80})`);\n }\n\n // Self-modifying guard, evaluated in the trusted gate (runs NO\n // PR-checked-out code): skip the ENTIRE job if the PR touches the\n // workflow, skill, local CLI, or any agent config the runner loads,\n // so a PR can\'t rewrite what runs and exfiltrate secrets.\n if (pr) {\n try {\n const files = await github.paginate(github.rest.pulls.listFiles, {\n owner: context.repo.owner,\n repo: context.repo.repo,\n pull_number: pr.number,\n per_page: 100,\n });\n const isSensitive = (p) =>\n p === \'.github/workflows/pr-visual-recap.yml\' ||\n /(^|\\/)skills\\/visual-(recap|plan|plans)\\//.test(p) ||\n /(^|\\/)\\.claude\\//.test(p) ||\n /(^|\\/)CLAUDE\\.md$/.test(p) ||\n /(^|\\/)AGENTS\\.md$/.test(p) ||\n /(^|\\/)\\.mcp\\.json$/.test(p) ||\n /(^|\\/)packages\\/core\\//.test(p);\n const hits = files.map((f) => f.filename).filter(isSensitive);\n if (hits.length) {\n reasons.push(`PR modifies recap-control files (${hits.slice(0, 3).join(\', \')}${hits.length > 3 ? \', …\' : \'\'}) — skipping so untrusted PR code never runs with secrets`);\n }\n } catch (e) {\n // Fail closed: if the file list can\'t be read, skip.\n reasons.push(`could not list PR files for the self-modifying guard (${e.message}); skipping to be safe`);\n }\n }\n\n const run = reasons.length === 0;\n core.setOutput(\'run\', run ? \'true\' : \'false\');\n core.setOutput(\'agent\', agent);\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join(\'; \')}`);\n\n recap:\n name: Generate visual recap\n needs: gate\n if: needs.gate.outputs.run == \'true\'\n runs-on: ubuntu-latest\n timeout-minutes: 30\n permissions:\n checks: write\n contents: read\n issues: write\n pull-requests: write\n env:\n PLAN_RECAP_APP_URL: ${{ secrets.PLAN_RECAP_APP_URL || \'https://plan.agent-native.com\' }}\n PLAN_RECAP_TOKEN: ${{ secrets.PLAN_RECAP_TOKEN }}\n GH_TOKEN: ${{ github.token }}\n PR_NUMBER: ${{ github.event.pull_request.number }}\n HEAD_SHA: ${{ github.event.pull_request.head.sha }}\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\n VISUAL_RECAP_REASONING: ${{ vars.VISUAL_RECAP_REASONING }}\n steps:\n - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n with:\n fetch-depth: 0\n # This job runs an agent over untrusted PR diff; don\'t leave the token\n # in .git/config (it uses GH_TOKEN for gh API calls, never git push).\n persist-credentials: false\n\n # Dogfood local source inside this monorepo, else the published package.\n # The pnpm steps run ONLY on the local path so npm/yarn consumer repos\n # (no pnpm-lock.yaml) fall back to `npx @agent-native/core`.\n - name: Resolve recap CLI\n id: cli\n run: |\n if [ -f packages/core/src/cli/index.ts ]; then\n echo "RECAP_CLI=pnpm exec tsx packages/core/src/cli/index.ts" >> "$GITHUB_ENV"\n echo "local=true" >> "$GITHUB_OUTPUT"\n else\n echo "RECAP_CLI=npx -y @agent-native/core@latest" >> "$GITHUB_ENV"\n echo "local=false" >> "$GITHUB_OUTPUT"\n fi\n\n - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8\n if: steps.cli.outputs.local == \'true\'\n\n - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version: "22"\n cache: ${{ steps.cli.outputs.local == \'true\' && \'pnpm\' || \'\' }}\n\n - name: Install workspace (local source only)\n if: steps.cli.outputs.local == \'true\'\n run: pnpm install --frozen-lockfile --ignore-scripts\n\n - name: Start visual recap check\n id: recap_check\n continue-on-error: true\n run: |\n set -uo pipefail\n $RECAP_CLI recap check start --sha "$HEAD_SHA" --workflow-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"\n\n - name: Collect bounded diff\n id: diff\n env:\n BASE_SHA: ${{ github.event.pull_request.base.sha }}\n run: |\n set -euo pipefail\n $RECAP_CLI recap collect-diff --base "$BASE_SHA" --head "$HEAD_SHA" --out recap.diff --stat recap.stat\n\n - name: Secret scan\n id: scan\n if: steps.diff.outputs.tiny != \'true\'\n run: |\n set -uo pipefail\n # Fail CLOSED: a scanner error or invalid JSON suppresses the diff so a\n # credential-bearing diff is never handed to the agent / plan service.\n if ! SCAN_JSON="$($RECAP_CLI recap scan --diff recap.diff)"; then\n SCAN_JSON=\'{"suppressed":true,"reason":"secret scan failed to run; failing closed"}\'\n fi\n {\n echo \'json<<__RECAP_SCAN_EOF__\'\n echo "$SCAN_JSON"\n echo \'__RECAP_SCAN_EOF__\'\n } >> "$GITHUB_OUTPUT"\n SUPPRESSED=$(node -e \'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?"true":"false")}catch{process.stdout.write("true")}\' "$SCAN_JSON")\n echo "suppressed=$SUPPRESSED" >> "$GITHUB_OUTPUT"\n\n - name: Read previous plan id\n id: prev\n continue-on-error: true\n run: |\n set -euo pipefail\n PLAN_ID="$($RECAP_CLI recap comment find-plan-id --repo "$GITHUB_REPOSITORY" --issue "$PR_NUMBER" --token "$GH_TOKEN")"\n echo "plan_id=$PLAN_ID" >> "$GITHUB_OUTPUT"\n\n - name: Build recap prompt\n id: prompt\n if: steps.diff.outputs.tiny != \'true\' && steps.scan.outputs.suppressed != \'true\'\n env:\n # Pass step outputs via env, NOT ${{ }} interpolation into the run body:\n # the prev plan id is parsed from a PR comment and could inject shell.\n PREV_PLAN_ID: ${{ steps.prev.outputs.plan_id }}\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\n run: |\n set -euo pipefail\n ARGS=(--diff recap.diff --stat recap.stat --pr "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --head "$HEAD_SHA" --app-url "$PLAN_RECAP_APP_URL" --out recap-prompt.md)\n if [ "${DIFF_HUGE:-}" = "true" ]; then ARGS+=(--huge); fi\n if [ -n "${PREV_PLAN_ID:-}" ]; then ARGS+=(--prev-plan-id "$PREV_PLAN_ID"); fi\n $RECAP_CLI recap build-prompt "${ARGS[@]}"\n\n - name: Run agent (Claude Code)\n id: claude\n if: needs.gate.outputs.agent == \'claude\' && steps.diff.outputs.tiny != \'true\' && steps.scan.outputs.suppressed != \'true\'\n continue-on-error: true\n env:\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n run: |\n set -uo pipefail\n MCP_CONFIG="$RUNNER_TEMP/plan-mcp.json"\n $RECAP_CLI recap mcp-config --agent claude --app-url "$PLAN_RECAP_APP_URL" --out "$MCP_CONFIG"\n CLAUDE_ARGS=(-p "$(cat recap-prompt.md)" --mcp-config "$MCP_CONFIG" --allowedTools "Read,Write,Bash(git diff:*),mcp__plan__get-plan-blocks,mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility" --permission-mode dontAsk --output-format json)\n if [ -n "${VISUAL_RECAP_MODEL:-}" ]; then CLAUDE_ARGS+=(--model "$VISUAL_RECAP_MODEL"); fi\n npx -y @anthropic-ai/claude-code@2 "${CLAUDE_ARGS[@]}" > claude-result.json || true\n rm -f "$MCP_CONFIG" || true\n\n - name: Run agent (Codex)\n id: codex\n if: needs.gate.outputs.agent == \'codex\' && steps.diff.outputs.tiny != \'true\' && steps.scan.outputs.suppressed != \'true\'\n continue-on-error: true\n env:\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n run: |\n set -uo pipefail\n $RECAP_CLI recap mcp-config --agent codex --app-url "$PLAN_RECAP_APP_URL"\n # `codex login` writes ~/.codex/auth.json (the bare env var is dropped on\n # the gpt-5.5 wss transport); stdin keeps the key out of process args.\n printenv OPENAI_API_KEY | npx -y @openai/codex@0 login --with-api-key || true\n # The runner is itself an ephemeral sandbox; bypass Codex\'s own sandbox\n # (bubblewrap can\'t init here) and approval gate (cancels the MCP write).\n CODEX_ARGS=(exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check)\n if [ -n "${VISUAL_RECAP_MODEL:-}" ]; then CODEX_ARGS+=(--model "$VISUAL_RECAP_MODEL"); fi\n # Validate reasoning against the enum before embedding it in the TOML override.\n case "${VISUAL_RECAP_REASONING:-}" in\n none|minimal|low|medium|high|xhigh)\n CODEX_ARGS+=(-c "model_reasoning_effort=\\"$VISUAL_RECAP_REASONING\\"") ;;\n "") ;;\n *) echo "Ignoring invalid VISUAL_RECAP_REASONING: $VISUAL_RECAP_REASONING" ;;\n esac\n npx -y @openai/codex@0 "${CODEX_ARGS[@]}" --json "$(cat recap-prompt.md)" | tee codex-events.jsonl || true\n\n - name: Read plan URL\n id: url\n if: steps.diff.outputs.tiny != \'true\' && steps.scan.outputs.suppressed != \'true\'\n run: |\n set -uo pipefail\n PLAN_URL=""\n if [ -f recap-url.txt ]; then PLAN_URL="$(tr -d \'\\r\\n\' < recap-url.txt | tr -d \' \')"; fi\n # recap-url.txt is agent-written → untrusted. Only proceed if its origin\n # matches the trusted plan app, so a prompt-injected URL can\'t drive the\n # screenshot/comment steps. Mismatch or empty → ok=false.\n OK=$(PLAN_URL="$PLAN_URL" node -e \'try{const u=new URL(process.env.PLAN_URL||"");const t=new URL(process.env.PLAN_RECAP_APP_URL||"https://plan.agent-native.com");process.stdout.write(u.origin===t.origin?"true":"false")}catch{process.stdout.write("false")}\')\n if [ "$OK" = "true" ]; then\n echo "plan_url=$PLAN_URL" >> "$GITHUB_OUTPUT"; echo "ok=true" >> "$GITHUB_OUTPUT"\n else\n echo "plan_url=" >> "$GITHUB_OUTPUT"; echo "ok=false" >> "$GITHUB_OUTPUT"\n fi\n\n - name: Attach usage\n if: steps.url.outputs.ok == \'true\'\n continue-on-error: true\n env:\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n # Use the gate-normalized agent so "Codex" still selects the right file.\n RECAP_AGENT: ${{ needs.gate.outputs.agent }}\n run: |\n set -uo pipefail\n RESULT=claude-result.json\n if [ "$RECAP_AGENT" = "codex" ]; then RESULT=codex-events.jsonl; fi\n if [ -f "$RESULT" ]; then $RECAP_CLI recap usage --plan-url "$PLAN_URL" --agent "$RECAP_AGENT" --result-file "$RESULT" --model "${VISUAL_RECAP_MODEL:-}" --app-url "$PLAN_RECAP_APP_URL" --token "$PLAN_RECAP_TOKEN" || true; fi\n\n - name: Screenshot + upload\n id: shot\n if: steps.url.outputs.ok == \'true\'\n continue-on-error: true\n env:\n # recap-url.txt is untrusted agent output; pass via env, never ${{ }}.\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n run: |\n set -uo pipefail\n pnpm exec playwright install --with-deps chromium 2>/dev/null || npx -y playwright@1 install --with-deps chromium || true\n SHOT_JSON="$($RECAP_CLI recap shot --url "$PLAN_URL" --token "$PLAN_RECAP_TOKEN" --app-url "$PLAN_RECAP_APP_URL" --out recap.png || echo \'{}\')"\n IMAGE_URL=$(node -e \'try{process.stdout.write(JSON.parse(process.argv[1]).imageUrl||"")}catch{process.stdout.write("")}\' "$SHOT_JSON")\n echo "image_url=$IMAGE_URL" >> "$GITHUB_OUTPUT"\n if [ -f recap.png ]; then echo "captured=true" >> "$GITHUB_OUTPUT"; else echo "captured=false" >> "$GITHUB_OUTPUT"; fi\n\n - name: Upload recap screenshot artifact\n if: steps.shot.outputs.captured == \'true\'\n uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n with:\n name: pr-visual-recap-${{ github.event.pull_request.number }}\n path: recap.png\n if-no-files-found: ignore\n retention-days: 14\n\n - name: Upsert sticky comment\n if: always()\n continue-on-error: true\n env:\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n RECAP_IMAGE_URL: ${{ steps.shot.outputs.image_url }}\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\n run: |\n set -euo pipefail\n ARGS=(recap comment upsert --repo "$GITHUB_REPOSITORY" --issue "$PR_NUMBER" --token "$GH_TOKEN")\n # On a tiny diff, only REFRESH an existing comment, never create one.\n if [ "${DIFF_TINY:-}" = "true" ]; then ARGS+=(--update-only); fi\n $RECAP_CLI "${ARGS[@]}"\n\n - name: Complete visual recap check\n if: always() && steps.recap_check.outputs.check_run_id != \'\'\n continue-on-error: true\n env:\n # Untrusted/step values via env (NOT ${{ }}-interpolated into the run\n # body): the agent-written plan URL and the scan JSON could inject shell.\n CHECK_RUN_ID: ${{ steps.recap_check.outputs.check_run_id }}\n PLAN_OK: ${{ steps.url.outputs.ok }}\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\n run: |\n set -uo pipefail\n $RECAP_CLI recap check complete \\\n --check-run-id "$CHECK_RUN_ID" \\\n --plan-ok "$PLAN_OK" \\\n --plan-url "$PLAN_URL" \\\n --suppressed "$SUPPRESSED" \\\n --suppressed-json "$SUPPRESSED_JSON" \\\n --huge "$DIFF_HUGE" \\\n --tiny "$DIFF_TINY" \\\n --workflow-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"\n';
10
+ export const PR_VISUAL_RECAP_WORKFLOW_YML = 'name: PR Visual Recap\n\n# Visual code review: a coding agent runs the repo\'s visual-recap skill over the\n# PR diff, publishes a plan, and upserts one sticky comment with a screenshot.\n# Plain `pull_request` (NOT `pull_request_target`) so fork code never sees secrets.\n\non:\n pull_request:\n types: [opened, synchronize, reopened, ready_for_review]\n\npermissions:\n contents: read\n\nconcurrency:\n group: pr-visual-recap-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n\nenv:\n VISUAL_RECAP_AGENT: ${{ vars.VISUAL_RECAP_AGENT || \'claude\' }}\n VISUAL_RECAP_SKILL_SOURCE: ${{ vars.VISUAL_RECAP_SKILL_SOURCE || \'auto\' }}\n\njobs:\n gate:\n name: Gate\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: read\n pull-requests: read\n outputs:\n run: ${{ steps.decide.outputs.run }}\n agent: ${{ steps.decide.outputs.agent }}\n steps:\n - id: decide\n uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\n env:\n # Presence-only signals — never expose secret VALUES to the gate.\n HAS_PLAN: ${{ secrets.PLAN_RECAP_TOKEN != \'\' }}\n HAS_ANTHROPIC: ${{ secrets.ANTHROPIC_API_KEY != \'\' }}\n HAS_OPENAI: ${{ secrets.OPENAI_API_KEY != \'\' }}\n AGENT: ${{ env.VISUAL_RECAP_AGENT }}\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\n with:\n script: |\n const pr = context.payload.pull_request;\n const reasons = [];\n\n if (!pr) reasons.push(\'no pull_request payload\');\n if (pr && pr.draft) reasons.push(\'draft PR\');\n\n // Fork PRs run with no secrets, so publishing would fail anyway — skip.\n const headRepo = pr && pr.head && pr.head.repo && pr.head.repo.full_name;\n if (pr && headRepo && headRepo !== process.env.GITHUB_REPOSITORY) {\n reasons.push(`fork PR (${headRepo})`);\n }\n\n const login = (pr && pr.user && pr.user.login || \'\').toLowerCase();\n const botAuthors = [\'dependabot[bot]\', \'dependabot\', \'renovate[bot]\', \'renovate\'];\n if (botAuthors.includes(login)) reasons.push(`bot author (${login})`);\n if (pr && pr.user && pr.user.type === \'Bot\') reasons.push(\'bot author (type=Bot)\');\n\n if (process.env.HAS_PLAN !== \'true\') reasons.push(\'PLAN_RECAP_TOKEN not configured\');\n\n // Normalize + validate the agent so a mis-cased value can\'t pass the\n // gate and then match neither agent step below.\n const agent = (process.env.AGENT || \'claude\').toLowerCase();\n if (agent !== \'claude\' && agent !== \'codex\') {\n reasons.push(`unsupported VISUAL_RECAP_AGENT "${process.env.AGENT}" (expected "claude" or "codex")`);\n } else if (agent === \'codex\') {\n if (process.env.HAS_OPENAI !== \'true\') reasons.push(\'OPENAI_API_KEY not configured (codex backend)\');\n } else {\n if (process.env.HAS_ANTHROPIC !== \'true\') reasons.push(\'ANTHROPIC_API_KEY not configured (claude backend)\');\n }\n\n // Validate the model before it reaches the agent CLI.\n const model = process.env.VISUAL_RECAP_MODEL || \'\';\n if (model && !/^[a-zA-Z0-9._-]{1,80}$/.test(model)) {\n reasons.push(`invalid VISUAL_RECAP_MODEL value (must match [a-zA-Z0-9._-]{1,80})`);\n }\n\n // Self-modifying guard, evaluated in the trusted gate (runs NO\n // PR-checked-out code): skip the ENTIRE job if the PR touches the\n // workflow, skill, local CLI, or any agent config the runner loads,\n // so a PR can\'t rewrite what runs and exfiltrate secrets.\n if (pr) {\n try {\n const files = await github.paginate(github.rest.pulls.listFiles, {\n owner: context.repo.owner,\n repo: context.repo.repo,\n pull_number: pr.number,\n per_page: 100,\n });\n const isAgentNativeMonorepo = context.repo.owner === \'BuilderIO\' && context.repo.repo === \'agent-native\';\n const isSensitive = (p) =>\n p === \'.github/workflows/pr-visual-recap.yml\' ||\n /(^|\\/)skills\\/visual-(recap|plan|plans)\\//.test(p) ||\n /(^|\\/)\\.claude\\//.test(p) ||\n /(^|\\/)CLAUDE\\.md$/.test(p) ||\n /(^|\\/)AGENTS\\.md$/.test(p) ||\n /(^|\\/)\\.mcp\\.json$/.test(p) ||\n (isAgentNativeMonorepo && /(^|\\/)packages\\/core\\//.test(p));\n const hits = files.map((f) => f.filename).filter(isSensitive);\n if (hits.length) {\n reasons.push(`PR modifies recap-control files (${hits.slice(0, 3).join(\', \')}${hits.length > 3 ? \', …\' : \'\'}) — skipping so untrusted PR code never runs with secrets`);\n }\n } catch (e) {\n // Fail closed: if the file list can\'t be read, skip.\n reasons.push(`could not list PR files for the self-modifying guard (${e.message}); skipping to be safe`);\n }\n }\n\n const run = reasons.length === 0;\n core.setOutput(\'run\', run ? \'true\' : \'false\');\n core.setOutput(\'agent\', agent);\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join(\'; \')}`);\n\n recap:\n name: Generate visual recap\n needs: gate\n if: needs.gate.outputs.run == \'true\'\n runs-on: ubuntu-latest\n timeout-minutes: 30\n permissions:\n checks: write\n contents: read\n issues: write\n pull-requests: write\n env:\n PLAN_RECAP_APP_URL: ${{ secrets.PLAN_RECAP_APP_URL || \'https://plan.agent-native.com\' }}\n PLAN_RECAP_TOKEN: ${{ secrets.PLAN_RECAP_TOKEN }}\n GH_TOKEN: ${{ github.token }}\n PR_NUMBER: ${{ github.event.pull_request.number }}\n HEAD_SHA: ${{ github.event.pull_request.head.sha }}\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\n VISUAL_RECAP_REASONING: ${{ vars.VISUAL_RECAP_REASONING }}\n VISUAL_RECAP_SKILL_SOURCE: ${{ vars.VISUAL_RECAP_SKILL_SOURCE || \'auto\' }}\n steps:\n - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\n with:\n fetch-depth: 0\n # This job runs an agent over untrusted PR diff; don\'t leave the token\n # in .git/config (it uses GH_TOKEN for gh API calls, never git push).\n persist-credentials: false\n\n # Dogfood local source inside this monorepo, else the published package.\n # The pnpm steps run ONLY on the local path so npm/yarn consumer repos\n # (no pnpm-lock.yaml) fall back to `npx @agent-native/core`.\n - name: Resolve recap CLI\n id: cli\n run: |\n if [ -f packages/core/src/cli/index.ts ]; then\n echo "RECAP_CLI=pnpm exec tsx packages/core/src/cli/index.ts" >> "$GITHUB_ENV"\n echo "local=true" >> "$GITHUB_OUTPUT"\n else\n echo "RECAP_CLI=npx -y @agent-native/core@latest" >> "$GITHUB_ENV"\n echo "local=false" >> "$GITHUB_OUTPUT"\n fi\n\n - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8\n if: steps.cli.outputs.local == \'true\'\n\n - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\n with:\n node-version: "22"\n cache: ${{ steps.cli.outputs.local == \'true\' && \'pnpm\' || \'\' }}\n\n - name: Install workspace (local source only)\n if: steps.cli.outputs.local == \'true\'\n run: pnpm install --frozen-lockfile --ignore-scripts\n\n - name: Start visual recap check\n id: recap_check\n continue-on-error: true\n run: |\n set -uo pipefail\n $RECAP_CLI recap check start --sha "$HEAD_SHA" --workflow-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"\n\n - name: Collect bounded diff\n id: diff\n env:\n BASE_SHA: ${{ github.event.pull_request.base.sha }}\n run: |\n set -euo pipefail\n $RECAP_CLI recap collect-diff --base "$BASE_SHA" --head "$HEAD_SHA" --out recap.diff --stat recap.stat\n\n - name: Secret scan\n id: scan\n if: steps.diff.outputs.tiny != \'true\'\n run: |\n set -uo pipefail\n # Fail CLOSED: a scanner error or invalid JSON suppresses the diff so a\n # credential-bearing diff is never handed to the agent / plan service.\n if ! SCAN_JSON="$($RECAP_CLI recap scan --diff recap.diff)"; then\n SCAN_JSON=\'{"suppressed":true,"reason":"secret scan failed to run; failing closed"}\'\n fi\n {\n echo \'json<<__RECAP_SCAN_EOF__\'\n echo "$SCAN_JSON"\n echo \'__RECAP_SCAN_EOF__\'\n } >> "$GITHUB_OUTPUT"\n SUPPRESSED=$(node -e \'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?"true":"false")}catch{process.stdout.write("true")}\' "$SCAN_JSON")\n echo "suppressed=$SUPPRESSED" >> "$GITHUB_OUTPUT"\n\n - name: Read previous plan id\n id: prev\n continue-on-error: true\n run: |\n set -euo pipefail\n PLAN_ID="$($RECAP_CLI recap comment find-plan-id --repo "$GITHUB_REPOSITORY" --issue "$PR_NUMBER" --token "$GH_TOKEN")"\n echo "plan_id=$PLAN_ID" >> "$GITHUB_OUTPUT"\n\n - name: Build recap prompt\n id: prompt\n if: steps.diff.outputs.tiny != \'true\' && steps.scan.outputs.suppressed != \'true\'\n env:\n # Pass step outputs via env, NOT ${{ }} interpolation into the run body:\n # the prev plan id is parsed from a PR comment and could inject shell.\n PREV_PLAN_ID: ${{ steps.prev.outputs.plan_id }}\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\n run: |\n set -euo pipefail\n ARGS=(--diff recap.diff --stat recap.stat --pr "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --head "$HEAD_SHA" --app-url "$PLAN_RECAP_APP_URL" --skill-source "$VISUAL_RECAP_SKILL_SOURCE" --out recap-prompt.md)\n if [ "${DIFF_HUGE:-}" = "true" ]; then ARGS+=(--huge); fi\n if [ -n "${PREV_PLAN_ID:-}" ]; then ARGS+=(--prev-plan-id "$PREV_PLAN_ID"); fi\n $RECAP_CLI recap build-prompt "${ARGS[@]}"\n\n - name: Run agent (Claude Code)\n id: claude\n if: needs.gate.outputs.agent == \'claude\' && steps.diff.outputs.tiny != \'true\' && steps.scan.outputs.suppressed != \'true\'\n continue-on-error: true\n env:\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n run: |\n set -uo pipefail\n MCP_CONFIG="$RUNNER_TEMP/plan-mcp.json"\n $RECAP_CLI recap mcp-config --agent claude --app-url "$PLAN_RECAP_APP_URL" --out "$MCP_CONFIG"\n CLAUDE_ARGS=(-p "$(cat recap-prompt.md)" --mcp-config "$MCP_CONFIG" --allowedTools "Read,Write,Bash(git diff:*),mcp__plan__get-plan-blocks,mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility" --permission-mode dontAsk --output-format json)\n if [ -n "${VISUAL_RECAP_MODEL:-}" ]; then CLAUDE_ARGS+=(--model "$VISUAL_RECAP_MODEL"); fi\n npx -y @anthropic-ai/claude-code@2 "${CLAUDE_ARGS[@]}" > claude-result.json || true\n rm -f "$MCP_CONFIG" || true\n\n - name: Run agent (Codex)\n id: codex\n if: needs.gate.outputs.agent == \'codex\' && steps.diff.outputs.tiny != \'true\' && steps.scan.outputs.suppressed != \'true\'\n continue-on-error: true\n env:\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n run: |\n set -uo pipefail\n $RECAP_CLI recap mcp-config --agent codex --app-url "$PLAN_RECAP_APP_URL"\n # `codex login` writes ~/.codex/auth.json (the bare env var is dropped on\n # the gpt-5.5 wss transport); stdin keeps the key out of process args.\n printenv OPENAI_API_KEY | npx -y @openai/codex@0 login --with-api-key || true\n # The runner is itself an ephemeral sandbox; bypass Codex\'s own sandbox\n # (bubblewrap can\'t init here) and approval gate (cancels the MCP write).\n CODEX_ARGS=(exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check)\n if [ -n "${VISUAL_RECAP_MODEL:-}" ]; then CODEX_ARGS+=(--model "$VISUAL_RECAP_MODEL"); fi\n # Validate reasoning against the enum before embedding it in the TOML override.\n case "${VISUAL_RECAP_REASONING:-}" in\n none|minimal|low|medium|high|xhigh)\n CODEX_ARGS+=(-c "model_reasoning_effort=\\"$VISUAL_RECAP_REASONING\\"") ;;\n "") ;;\n *) echo "Ignoring invalid VISUAL_RECAP_REASONING: $VISUAL_RECAP_REASONING" ;;\n esac\n npx -y @openai/codex@0 "${CODEX_ARGS[@]}" --json "$(cat recap-prompt.md)" | tee codex-events.jsonl || true\n\n - name: Read plan URL\n id: url\n if: steps.diff.outputs.tiny != \'true\' && steps.scan.outputs.suppressed != \'true\'\n run: |\n set -uo pipefail\n PLAN_URL=""\n if [ -f recap-url.txt ]; then PLAN_URL="$(tr -d \'\\r\\n\' < recap-url.txt | tr -d \' \')"; fi\n # recap-url.txt is agent-written -> untrusted. Rebuild a canonical\n # recap URL from the trusted app base and a strictly validated plan id,\n # preserving path-prefixed self-hosted mounts.\n CANONICAL_URL=$(PLAN_URL="$PLAN_URL" node <<\'NODE\'\n try {\n const raw = process.env.PLAN_URL || "";\n const trusted = new URL(process.env.PLAN_RECAP_APP_URL || "https://plan.agent-native.com");\n const parsed = /^https?:\\/\\//i.test(raw)\n ? new URL(raw)\n : new URL(raw, trusted);\n if (parsed.origin !== trusted.origin) {\n process.exit(0);\n }\n\n const base = trusted.pathname.replace(/\\/$/, "");\n const paths = [parsed.pathname];\n if (base && parsed.pathname.startsWith(`${base}/`)) {\n paths.push(parsed.pathname.slice(base.length) || "/");\n }\n\n for (const path of paths) {\n const match = path.match(/^\\/(?:plans|recaps)\\/([A-Za-z0-9_-]+)\\/?$/);\n if (match) {\n process.stdout.write(`${trusted.origin}${base}/recaps/${match[1]}`);\n break;\n }\n }\n } catch {\n process.exit(0);\n }\n NODE\n )\n if [ -n "$CANONICAL_URL" ]; then\n echo "plan_url=$CANONICAL_URL" >> "$GITHUB_OUTPUT"; echo "ok=true" >> "$GITHUB_OUTPUT"\n else\n echo "plan_url=" >> "$GITHUB_OUTPUT"; echo "ok=false" >> "$GITHUB_OUTPUT"\n fi\n\n - name: Attach usage\n if: steps.url.outputs.ok == \'true\'\n continue-on-error: true\n env:\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n # Use the gate-normalized agent so "Codex" still selects the right file.\n RECAP_AGENT: ${{ needs.gate.outputs.agent }}\n run: |\n set -uo pipefail\n RESULT=claude-result.json\n if [ "$RECAP_AGENT" = "codex" ]; then RESULT=codex-events.jsonl; fi\n if [ -f "$RESULT" ]; then $RECAP_CLI recap usage --plan-url "$PLAN_URL" --agent "$RECAP_AGENT" --result-file "$RESULT" --model "${VISUAL_RECAP_MODEL:-}" --app-url "$PLAN_RECAP_APP_URL" --token "$PLAN_RECAP_TOKEN" || true; fi\n\n - name: Cache Playwright browsers\n if: steps.url.outputs.ok == \'true\'\n uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3\n with:\n path: ~/.cache/ms-playwright\n key: playwright-1-${{ runner.os }}\n\n - name: Screenshot + upload\n id: shot\n if: steps.url.outputs.ok == \'true\'\n continue-on-error: true\n env:\n # recap-url.txt is untrusted agent output; pass via env, never ${{ }}.\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n run: |\n set -uo pipefail\n pnpm exec playwright install --with-deps chromium 2>/dev/null || npx -y playwright@1 install --with-deps chromium || true\n SHOT_JSON="$($RECAP_CLI recap shot --url "$PLAN_URL" --token "$PLAN_RECAP_TOKEN" --app-url "$PLAN_RECAP_APP_URL" --out recap.png || echo \'{}\')"\n IMAGE_URL=$(node -e \'try{process.stdout.write(JSON.parse(process.argv[1]).imageUrl||"")}catch{process.stdout.write("")}\' "$SHOT_JSON")\n echo "image_url=$IMAGE_URL" >> "$GITHUB_OUTPUT"\n if [ -f recap.png ]; then echo "captured=true" >> "$GITHUB_OUTPUT"; else echo "captured=false" >> "$GITHUB_OUTPUT"; fi\n\n - name: Upload recap screenshot artifact\n if: steps.shot.outputs.captured == \'true\'\n uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\n with:\n name: pr-visual-recap-${{ github.event.pull_request.number }}\n path: recap.png\n if-no-files-found: ignore\n retention-days: 14\n\n - name: Upsert sticky comment\n if: always()\n continue-on-error: true\n env:\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n RECAP_IMAGE_URL: ${{ steps.shot.outputs.image_url }}\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\n PREV_PLAN_ID: ${{ steps.prev.outputs.plan_id }}\n run: |\n set -euo pipefail\n ARGS=(recap comment upsert --repo "$GITHUB_REPOSITORY" --issue "$PR_NUMBER" --token "$GH_TOKEN")\n # On a tiny diff, only REFRESH an existing comment, never create one.\n if [ "${DIFF_TINY:-}" = "true" ]; then ARGS+=(--update-only); fi\n $RECAP_CLI "${ARGS[@]}"\n\n - name: Complete visual recap check\n if: always() && steps.recap_check.outputs.check_run_id != \'\'\n continue-on-error: true\n env:\n # Untrusted/step values via env (NOT ${{ }}-interpolated into the run\n # body): the agent-written plan URL and the scan JSON could inject shell.\n CHECK_RUN_ID: ${{ steps.recap_check.outputs.check_run_id }}\n PLAN_OK: ${{ steps.url.outputs.ok }}\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\n run: |\n set -uo pipefail\n $RECAP_CLI recap check complete \\\n --check-run-id "$CHECK_RUN_ID" \\\n --plan-ok "$PLAN_OK" \\\n --plan-url "$PLAN_URL" \\\n --suppressed "$SUPPRESSED" \\\n --suppressed-json "$SUPPRESSED_JSON" \\\n --huge "$DIFF_HUGE" \\\n --tiny "$DIFF_TINY" \\\n --workflow-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID"\n';
11
11
  //# sourceMappingURL=pr-visual-recap-workflow.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pr-visual-recap-workflow.js","sourceRoot":"","sources":["../../src/cli/pr-visual-recap-workflow.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,CAAC,MAAM,4BAA4B,GACvC,mkjBAAmkjB,CAAC","sourcesContent":["/**\n * Bundled copy of .github/workflows/pr-visual-recap.yml so the CLI can write the\n * PR Visual Recap workflow into a user repo via\n * `agent-native skills add visual-plan --with-github-action`.\n *\n * AUTO-GENERATED — keep byte-identical with the source workflow. A sync test in\n * recap.spec.ts fails if these drift. Regenerate from the YAML with the snippet\n * in recap.spec.ts.\n */\n\nexport const PR_VISUAL_RECAP_WORKFLOW_YML =\n 'name: PR Visual Recap\\n\\n# Visual code review: a coding agent runs the repo\\'s visual-recap skill over the\\n# PR diff, publishes a plan, and upserts one sticky comment with a screenshot.\\n# Plain `pull_request` (NOT `pull_request_target`) so fork code never sees secrets.\\n\\non:\\n pull_request:\\n types: [opened, synchronize, reopened, ready_for_review]\\n\\npermissions:\\n contents: read\\n\\nconcurrency:\\n group: pr-visual-recap-${{ github.event.pull_request.number }}\\n cancel-in-progress: true\\n\\nenv:\\n VISUAL_RECAP_AGENT: ${{ vars.VISUAL_RECAP_AGENT || \\'claude\\' }}\\n\\njobs:\\n gate:\\n name: Gate\\n runs-on: ubuntu-latest\\n timeout-minutes: 10\\n permissions:\\n contents: read\\n pull-requests: read\\n outputs:\\n run: ${{ steps.decide.outputs.run }}\\n agent: ${{ steps.decide.outputs.agent }}\\n steps:\\n - id: decide\\n uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\\n env:\\n # Presence-only signals — never expose secret VALUES to the gate.\\n HAS_PLAN: ${{ secrets.PLAN_RECAP_TOKEN != \\'\\' }}\\n HAS_ANTHROPIC: ${{ secrets.ANTHROPIC_API_KEY != \\'\\' }}\\n HAS_OPENAI: ${{ secrets.OPENAI_API_KEY != \\'\\' }}\\n AGENT: ${{ env.VISUAL_RECAP_AGENT }}\\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\\n with:\\n script: |\\n const pr = context.payload.pull_request;\\n const reasons = [];\\n\\n if (!pr) reasons.push(\\'no pull_request payload\\');\\n if (pr && pr.draft) reasons.push(\\'draft PR\\');\\n\\n // Fork PRs run with no secrets, so publishing would fail anyway — skip.\\n const headRepo = pr && pr.head && pr.head.repo && pr.head.repo.full_name;\\n if (pr && headRepo && headRepo !== process.env.GITHUB_REPOSITORY) {\\n reasons.push(`fork PR (${headRepo})`);\\n }\\n\\n const login = (pr && pr.user && pr.user.login || \\'\\').toLowerCase();\\n const botAuthors = [\\'dependabot[bot]\\', \\'dependabot\\', \\'renovate[bot]\\', \\'renovate\\'];\\n if (botAuthors.includes(login)) reasons.push(`bot author (${login})`);\\n if (pr && pr.user && pr.user.type === \\'Bot\\') reasons.push(\\'bot author (type=Bot)\\');\\n\\n if (process.env.HAS_PLAN !== \\'true\\') reasons.push(\\'PLAN_RECAP_TOKEN not configured\\');\\n\\n // Normalize + validate the agent so a mis-cased value can\\'t pass the\\n // gate and then match neither agent step below.\\n const agent = (process.env.AGENT || \\'claude\\').toLowerCase();\\n if (agent !== \\'claude\\' && agent !== \\'codex\\') {\\n reasons.push(`unsupported VISUAL_RECAP_AGENT \"${process.env.AGENT}\" (expected \"claude\" or \"codex\")`);\\n } else if (agent === \\'codex\\') {\\n if (process.env.HAS_OPENAI !== \\'true\\') reasons.push(\\'OPENAI_API_KEY not configured (codex backend)\\');\\n } else {\\n if (process.env.HAS_ANTHROPIC !== \\'true\\') reasons.push(\\'ANTHROPIC_API_KEY not configured (claude backend)\\');\\n }\\n\\n // Validate the model before it reaches the agent CLI.\\n const model = process.env.VISUAL_RECAP_MODEL || \\'\\';\\n if (model && !/^[a-zA-Z0-9._-]{1,80}$/.test(model)) {\\n reasons.push(`invalid VISUAL_RECAP_MODEL value (must match [a-zA-Z0-9._-]{1,80})`);\\n }\\n\\n // Self-modifying guard, evaluated in the trusted gate (runs NO\\n // PR-checked-out code): skip the ENTIRE job if the PR touches the\\n // workflow, skill, local CLI, or any agent config the runner loads,\\n // so a PR can\\'t rewrite what runs and exfiltrate secrets.\\n if (pr) {\\n try {\\n const files = await github.paginate(github.rest.pulls.listFiles, {\\n owner: context.repo.owner,\\n repo: context.repo.repo,\\n pull_number: pr.number,\\n per_page: 100,\\n });\\n const isSensitive = (p) =>\\n p === \\'.github/workflows/pr-visual-recap.yml\\' ||\\n /(^|\\\\/)skills\\\\/visual-(recap|plan|plans)\\\\//.test(p) ||\\n /(^|\\\\/)\\\\.claude\\\\//.test(p) ||\\n /(^|\\\\/)CLAUDE\\\\.md$/.test(p) ||\\n /(^|\\\\/)AGENTS\\\\.md$/.test(p) ||\\n /(^|\\\\/)\\\\.mcp\\\\.json$/.test(p) ||\\n /(^|\\\\/)packages\\\\/core\\\\//.test(p);\\n const hits = files.map((f) => f.filename).filter(isSensitive);\\n if (hits.length) {\\n reasons.push(`PR modifies recap-control files (${hits.slice(0, 3).join(\\', \\')}${hits.length > 3 ? \\', …\\' : \\'\\'}) — skipping so untrusted PR code never runs with secrets`);\\n }\\n } catch (e) {\\n // Fail closed: if the file list can\\'t be read, skip.\\n reasons.push(`could not list PR files for the self-modifying guard (${e.message}); skipping to be safe`);\\n }\\n }\\n\\n const run = reasons.length === 0;\\n core.setOutput(\\'run\\', run ? \\'true\\' : \\'false\\');\\n core.setOutput(\\'agent\\', agent);\\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join(\\'; \\')}`);\\n\\n recap:\\n name: Generate visual recap\\n needs: gate\\n if: needs.gate.outputs.run == \\'true\\'\\n runs-on: ubuntu-latest\\n timeout-minutes: 30\\n permissions:\\n checks: write\\n contents: read\\n issues: write\\n pull-requests: write\\n env:\\n PLAN_RECAP_APP_URL: ${{ secrets.PLAN_RECAP_APP_URL || \\'https://plan.agent-native.com\\' }}\\n PLAN_RECAP_TOKEN: ${{ secrets.PLAN_RECAP_TOKEN }}\\n GH_TOKEN: ${{ github.token }}\\n PR_NUMBER: ${{ github.event.pull_request.number }}\\n HEAD_SHA: ${{ github.event.pull_request.head.sha }}\\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\\n VISUAL_RECAP_REASONING: ${{ vars.VISUAL_RECAP_REASONING }}\\n steps:\\n - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\\n with:\\n fetch-depth: 0\\n # This job runs an agent over untrusted PR diff; don\\'t leave the token\\n # in .git/config (it uses GH_TOKEN for gh API calls, never git push).\\n persist-credentials: false\\n\\n # Dogfood local source inside this monorepo, else the published package.\\n # The pnpm steps run ONLY on the local path so npm/yarn consumer repos\\n # (no pnpm-lock.yaml) fall back to `npx @agent-native/core`.\\n - name: Resolve recap CLI\\n id: cli\\n run: |\\n if [ -f packages/core/src/cli/index.ts ]; then\\n echo \"RECAP_CLI=pnpm exec tsx packages/core/src/cli/index.ts\" >> \"$GITHUB_ENV\"\\n echo \"local=true\" >> \"$GITHUB_OUTPUT\"\\n else\\n echo \"RECAP_CLI=npx -y @agent-native/core@latest\" >> \"$GITHUB_ENV\"\\n echo \"local=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8\\n if: steps.cli.outputs.local == \\'true\\'\\n\\n - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\\n with:\\n node-version: \"22\"\\n cache: ${{ steps.cli.outputs.local == \\'true\\' && \\'pnpm\\' || \\'\\' }}\\n\\n - name: Install workspace (local source only)\\n if: steps.cli.outputs.local == \\'true\\'\\n run: pnpm install --frozen-lockfile --ignore-scripts\\n\\n - name: Start visual recap check\\n id: recap_check\\n continue-on-error: true\\n run: |\\n set -uo pipefail\\n $RECAP_CLI recap check start --sha \"$HEAD_SHA\" --workflow-url \"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID\"\\n\\n - name: Collect bounded diff\\n id: diff\\n env:\\n BASE_SHA: ${{ github.event.pull_request.base.sha }}\\n run: |\\n set -euo pipefail\\n $RECAP_CLI recap collect-diff --base \"$BASE_SHA\" --head \"$HEAD_SHA\" --out recap.diff --stat recap.stat\\n\\n - name: Secret scan\\n id: scan\\n if: steps.diff.outputs.tiny != \\'true\\'\\n run: |\\n set -uo pipefail\\n # Fail CLOSED: a scanner error or invalid JSON suppresses the diff so a\\n # credential-bearing diff is never handed to the agent / plan service.\\n if ! SCAN_JSON=\"$($RECAP_CLI recap scan --diff recap.diff)\"; then\\n SCAN_JSON=\\'{\"suppressed\":true,\"reason\":\"secret scan failed to run; failing closed\"}\\'\\n fi\\n {\\n echo \\'json<<__RECAP_SCAN_EOF__\\'\\n echo \"$SCAN_JSON\"\\n echo \\'__RECAP_SCAN_EOF__\\'\\n } >> \"$GITHUB_OUTPUT\"\\n SUPPRESSED=$(node -e \\'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?\"true\":\"false\")}catch{process.stdout.write(\"true\")}\\' \"$SCAN_JSON\")\\n echo \"suppressed=$SUPPRESSED\" >> \"$GITHUB_OUTPUT\"\\n\\n - name: Read previous plan id\\n id: prev\\n continue-on-error: true\\n run: |\\n set -euo pipefail\\n PLAN_ID=\"$($RECAP_CLI recap comment find-plan-id --repo \"$GITHUB_REPOSITORY\" --issue \"$PR_NUMBER\" --token \"$GH_TOKEN\")\"\\n echo \"plan_id=$PLAN_ID\" >> \"$GITHUB_OUTPUT\"\\n\\n - name: Build recap prompt\\n id: prompt\\n if: steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n env:\\n # Pass step outputs via env, NOT ${{ }} interpolation into the run body:\\n # the prev plan id is parsed from a PR comment and could inject shell.\\n PREV_PLAN_ID: ${{ steps.prev.outputs.plan_id }}\\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\\n run: |\\n set -euo pipefail\\n ARGS=(--diff recap.diff --stat recap.stat --pr \"$PR_NUMBER\" --repo \"$GITHUB_REPOSITORY\" --head \"$HEAD_SHA\" --app-url \"$PLAN_RECAP_APP_URL\" --out recap-prompt.md)\\n if [ \"${DIFF_HUGE:-}\" = \"true\" ]; then ARGS+=(--huge); fi\\n if [ -n \"${PREV_PLAN_ID:-}\" ]; then ARGS+=(--prev-plan-id \"$PREV_PLAN_ID\"); fi\\n $RECAP_CLI recap build-prompt \"${ARGS[@]}\"\\n\\n - name: Run agent (Claude Code)\\n id: claude\\n if: needs.gate.outputs.agent == \\'claude\\' && steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n continue-on-error: true\\n env:\\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\\n run: |\\n set -uo pipefail\\n MCP_CONFIG=\"$RUNNER_TEMP/plan-mcp.json\"\\n $RECAP_CLI recap mcp-config --agent claude --app-url \"$PLAN_RECAP_APP_URL\" --out \"$MCP_CONFIG\"\\n CLAUDE_ARGS=(-p \"$(cat recap-prompt.md)\" --mcp-config \"$MCP_CONFIG\" --allowedTools \"Read,Write,Bash(git diff:*),mcp__plan__get-plan-blocks,mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility\" --permission-mode dontAsk --output-format json)\\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CLAUDE_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\\n npx -y @anthropic-ai/claude-code@2 \"${CLAUDE_ARGS[@]}\" > claude-result.json || true\\n rm -f \"$MCP_CONFIG\" || true\\n\\n - name: Run agent (Codex)\\n id: codex\\n if: needs.gate.outputs.agent == \\'codex\\' && steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n continue-on-error: true\\n env:\\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\\n run: |\\n set -uo pipefail\\n $RECAP_CLI recap mcp-config --agent codex --app-url \"$PLAN_RECAP_APP_URL\"\\n # `codex login` writes ~/.codex/auth.json (the bare env var is dropped on\\n # the gpt-5.5 wss transport); stdin keeps the key out of process args.\\n printenv OPENAI_API_KEY | npx -y @openai/codex@0 login --with-api-key || true\\n # The runner is itself an ephemeral sandbox; bypass Codex\\'s own sandbox\\n # (bubblewrap can\\'t init here) and approval gate (cancels the MCP write).\\n CODEX_ARGS=(exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check)\\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CODEX_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\\n # Validate reasoning against the enum before embedding it in the TOML override.\\n case \"${VISUAL_RECAP_REASONING:-}\" in\\n none|minimal|low|medium|high|xhigh)\\n CODEX_ARGS+=(-c \"model_reasoning_effort=\\\\\"$VISUAL_RECAP_REASONING\\\\\"\") ;;\\n \"\") ;;\\n *) echo \"Ignoring invalid VISUAL_RECAP_REASONING: $VISUAL_RECAP_REASONING\" ;;\\n esac\\n npx -y @openai/codex@0 \"${CODEX_ARGS[@]}\" --json \"$(cat recap-prompt.md)\" | tee codex-events.jsonl || true\\n\\n - name: Read plan URL\\n id: url\\n if: steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n run: |\\n set -uo pipefail\\n PLAN_URL=\"\"\\n if [ -f recap-url.txt ]; then PLAN_URL=\"$(tr -d \\'\\\\r\\\\n\\' < recap-url.txt | tr -d \\' \\')\"; fi\\n # recap-url.txt is agent-written → untrusted. Only proceed if its origin\\n # matches the trusted plan app, so a prompt-injected URL can\\'t drive the\\n # screenshot/comment steps. Mismatch or empty → ok=false.\\n OK=$(PLAN_URL=\"$PLAN_URL\" node -e \\'try{const u=new URL(process.env.PLAN_URL||\"\");const t=new URL(process.env.PLAN_RECAP_APP_URL||\"https://plan.agent-native.com\");process.stdout.write(u.origin===t.origin?\"true\":\"false\")}catch{process.stdout.write(\"false\")}\\')\\n if [ \"$OK\" = \"true\" ]; then\\n echo \"plan_url=$PLAN_URL\" >> \"$GITHUB_OUTPUT\"; echo \"ok=true\" >> \"$GITHUB_OUTPUT\"\\n else\\n echo \"plan_url=\" >> \"$GITHUB_OUTPUT\"; echo \"ok=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n - name: Attach usage\\n if: steps.url.outputs.ok == \\'true\\'\\n continue-on-error: true\\n env:\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n # Use the gate-normalized agent so \"Codex\" still selects the right file.\\n RECAP_AGENT: ${{ needs.gate.outputs.agent }}\\n run: |\\n set -uo pipefail\\n RESULT=claude-result.json\\n if [ \"$RECAP_AGENT\" = \"codex\" ]; then RESULT=codex-events.jsonl; fi\\n if [ -f \"$RESULT\" ]; then $RECAP_CLI recap usage --plan-url \"$PLAN_URL\" --agent \"$RECAP_AGENT\" --result-file \"$RESULT\" --model \"${VISUAL_RECAP_MODEL:-}\" --app-url \"$PLAN_RECAP_APP_URL\" --token \"$PLAN_RECAP_TOKEN\" || true; fi\\n\\n - name: Screenshot + upload\\n id: shot\\n if: steps.url.outputs.ok == \\'true\\'\\n continue-on-error: true\\n env:\\n # recap-url.txt is untrusted agent output; pass via env, never ${{ }}.\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n run: |\\n set -uo pipefail\\n pnpm exec playwright install --with-deps chromium 2>/dev/null || npx -y playwright@1 install --with-deps chromium || true\\n SHOT_JSON=\"$($RECAP_CLI recap shot --url \"$PLAN_URL\" --token \"$PLAN_RECAP_TOKEN\" --app-url \"$PLAN_RECAP_APP_URL\" --out recap.png || echo \\'{}\\')\"\\n IMAGE_URL=$(node -e \\'try{process.stdout.write(JSON.parse(process.argv[1]).imageUrl||\"\")}catch{process.stdout.write(\"\")}\\' \"$SHOT_JSON\")\\n echo \"image_url=$IMAGE_URL\" >> \"$GITHUB_OUTPUT\"\\n if [ -f recap.png ]; then echo \"captured=true\" >> \"$GITHUB_OUTPUT\"; else echo \"captured=false\" >> \"$GITHUB_OUTPUT\"; fi\\n\\n - name: Upload recap screenshot artifact\\n if: steps.shot.outputs.captured == \\'true\\'\\n uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\\n with:\\n name: pr-visual-recap-${{ github.event.pull_request.number }}\\n path: recap.png\\n if-no-files-found: ignore\\n retention-days: 14\\n\\n - name: Upsert sticky comment\\n if: always()\\n continue-on-error: true\\n env:\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n RECAP_IMAGE_URL: ${{ steps.shot.outputs.image_url }}\\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\\n run: |\\n set -euo pipefail\\n ARGS=(recap comment upsert --repo \"$GITHUB_REPOSITORY\" --issue \"$PR_NUMBER\" --token \"$GH_TOKEN\")\\n # On a tiny diff, only REFRESH an existing comment, never create one.\\n if [ \"${DIFF_TINY:-}\" = \"true\" ]; then ARGS+=(--update-only); fi\\n $RECAP_CLI \"${ARGS[@]}\"\\n\\n - name: Complete visual recap check\\n if: always() && steps.recap_check.outputs.check_run_id != \\'\\'\\n continue-on-error: true\\n env:\\n # Untrusted/step values via env (NOT ${{ }}-interpolated into the run\\n # body): the agent-written plan URL and the scan JSON could inject shell.\\n CHECK_RUN_ID: ${{ steps.recap_check.outputs.check_run_id }}\\n PLAN_OK: ${{ steps.url.outputs.ok }}\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\\n run: |\\n set -uo pipefail\\n $RECAP_CLI recap check complete \\\\\\n --check-run-id \"$CHECK_RUN_ID\" \\\\\\n --plan-ok \"$PLAN_OK\" \\\\\\n --plan-url \"$PLAN_URL\" \\\\\\n --suppressed \"$SUPPRESSED\" \\\\\\n --suppressed-json \"$SUPPRESSED_JSON\" \\\\\\n --huge \"$DIFF_HUGE\" \\\\\\n --tiny \"$DIFF_TINY\" \\\\\\n --workflow-url \"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID\"\\n';\n"]}
1
+ {"version":3,"file":"pr-visual-recap-workflow.js","sourceRoot":"","sources":["../../src/cli/pr-visual-recap-workflow.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,CAAC,MAAM,4BAA4B,GACvC,ikmBAAikmB,CAAC","sourcesContent":["/**\n * Bundled copy of .github/workflows/pr-visual-recap.yml so the CLI can write the\n * PR Visual Recap workflow into a user repo via\n * `agent-native skills add visual-plan --with-github-action`.\n *\n * AUTO-GENERATED — keep byte-identical with the source workflow. A sync test in\n * recap.spec.ts fails if these drift. Regenerate from the YAML with the snippet\n * in recap.spec.ts.\n */\n\nexport const PR_VISUAL_RECAP_WORKFLOW_YML =\n 'name: PR Visual Recap\\n\\n# Visual code review: a coding agent runs the repo\\'s visual-recap skill over the\\n# PR diff, publishes a plan, and upserts one sticky comment with a screenshot.\\n# Plain `pull_request` (NOT `pull_request_target`) so fork code never sees secrets.\\n\\non:\\n pull_request:\\n types: [opened, synchronize, reopened, ready_for_review]\\n\\npermissions:\\n contents: read\\n\\nconcurrency:\\n group: pr-visual-recap-${{ github.event.pull_request.number }}\\n cancel-in-progress: true\\n\\nenv:\\n VISUAL_RECAP_AGENT: ${{ vars.VISUAL_RECAP_AGENT || \\'claude\\' }}\\n VISUAL_RECAP_SKILL_SOURCE: ${{ vars.VISUAL_RECAP_SKILL_SOURCE || \\'auto\\' }}\\n\\njobs:\\n gate:\\n name: Gate\\n runs-on: ubuntu-latest\\n timeout-minutes: 10\\n permissions:\\n contents: read\\n pull-requests: read\\n outputs:\\n run: ${{ steps.decide.outputs.run }}\\n agent: ${{ steps.decide.outputs.agent }}\\n steps:\\n - id: decide\\n uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\\n env:\\n # Presence-only signals — never expose secret VALUES to the gate.\\n HAS_PLAN: ${{ secrets.PLAN_RECAP_TOKEN != \\'\\' }}\\n HAS_ANTHROPIC: ${{ secrets.ANTHROPIC_API_KEY != \\'\\' }}\\n HAS_OPENAI: ${{ secrets.OPENAI_API_KEY != \\'\\' }}\\n AGENT: ${{ env.VISUAL_RECAP_AGENT }}\\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\\n with:\\n script: |\\n const pr = context.payload.pull_request;\\n const reasons = [];\\n\\n if (!pr) reasons.push(\\'no pull_request payload\\');\\n if (pr && pr.draft) reasons.push(\\'draft PR\\');\\n\\n // Fork PRs run with no secrets, so publishing would fail anyway — skip.\\n const headRepo = pr && pr.head && pr.head.repo && pr.head.repo.full_name;\\n if (pr && headRepo && headRepo !== process.env.GITHUB_REPOSITORY) {\\n reasons.push(`fork PR (${headRepo})`);\\n }\\n\\n const login = (pr && pr.user && pr.user.login || \\'\\').toLowerCase();\\n const botAuthors = [\\'dependabot[bot]\\', \\'dependabot\\', \\'renovate[bot]\\', \\'renovate\\'];\\n if (botAuthors.includes(login)) reasons.push(`bot author (${login})`);\\n if (pr && pr.user && pr.user.type === \\'Bot\\') reasons.push(\\'bot author (type=Bot)\\');\\n\\n if (process.env.HAS_PLAN !== \\'true\\') reasons.push(\\'PLAN_RECAP_TOKEN not configured\\');\\n\\n // Normalize + validate the agent so a mis-cased value can\\'t pass the\\n // gate and then match neither agent step below.\\n const agent = (process.env.AGENT || \\'claude\\').toLowerCase();\\n if (agent !== \\'claude\\' && agent !== \\'codex\\') {\\n reasons.push(`unsupported VISUAL_RECAP_AGENT \"${process.env.AGENT}\" (expected \"claude\" or \"codex\")`);\\n } else if (agent === \\'codex\\') {\\n if (process.env.HAS_OPENAI !== \\'true\\') reasons.push(\\'OPENAI_API_KEY not configured (codex backend)\\');\\n } else {\\n if (process.env.HAS_ANTHROPIC !== \\'true\\') reasons.push(\\'ANTHROPIC_API_KEY not configured (claude backend)\\');\\n }\\n\\n // Validate the model before it reaches the agent CLI.\\n const model = process.env.VISUAL_RECAP_MODEL || \\'\\';\\n if (model && !/^[a-zA-Z0-9._-]{1,80}$/.test(model)) {\\n reasons.push(`invalid VISUAL_RECAP_MODEL value (must match [a-zA-Z0-9._-]{1,80})`);\\n }\\n\\n // Self-modifying guard, evaluated in the trusted gate (runs NO\\n // PR-checked-out code): skip the ENTIRE job if the PR touches the\\n // workflow, skill, local CLI, or any agent config the runner loads,\\n // so a PR can\\'t rewrite what runs and exfiltrate secrets.\\n if (pr) {\\n try {\\n const files = await github.paginate(github.rest.pulls.listFiles, {\\n owner: context.repo.owner,\\n repo: context.repo.repo,\\n pull_number: pr.number,\\n per_page: 100,\\n });\\n const isAgentNativeMonorepo = context.repo.owner === \\'BuilderIO\\' && context.repo.repo === \\'agent-native\\';\\n const isSensitive = (p) =>\\n p === \\'.github/workflows/pr-visual-recap.yml\\' ||\\n /(^|\\\\/)skills\\\\/visual-(recap|plan|plans)\\\\//.test(p) ||\\n /(^|\\\\/)\\\\.claude\\\\//.test(p) ||\\n /(^|\\\\/)CLAUDE\\\\.md$/.test(p) ||\\n /(^|\\\\/)AGENTS\\\\.md$/.test(p) ||\\n /(^|\\\\/)\\\\.mcp\\\\.json$/.test(p) ||\\n (isAgentNativeMonorepo && /(^|\\\\/)packages\\\\/core\\\\//.test(p));\\n const hits = files.map((f) => f.filename).filter(isSensitive);\\n if (hits.length) {\\n reasons.push(`PR modifies recap-control files (${hits.slice(0, 3).join(\\', \\')}${hits.length > 3 ? \\', …\\' : \\'\\'}) — skipping so untrusted PR code never runs with secrets`);\\n }\\n } catch (e) {\\n // Fail closed: if the file list can\\'t be read, skip.\\n reasons.push(`could not list PR files for the self-modifying guard (${e.message}); skipping to be safe`);\\n }\\n }\\n\\n const run = reasons.length === 0;\\n core.setOutput(\\'run\\', run ? \\'true\\' : \\'false\\');\\n core.setOutput(\\'agent\\', agent);\\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join(\\'; \\')}`);\\n\\n recap:\\n name: Generate visual recap\\n needs: gate\\n if: needs.gate.outputs.run == \\'true\\'\\n runs-on: ubuntu-latest\\n timeout-minutes: 30\\n permissions:\\n checks: write\\n contents: read\\n issues: write\\n pull-requests: write\\n env:\\n PLAN_RECAP_APP_URL: ${{ secrets.PLAN_RECAP_APP_URL || \\'https://plan.agent-native.com\\' }}\\n PLAN_RECAP_TOKEN: ${{ secrets.PLAN_RECAP_TOKEN }}\\n GH_TOKEN: ${{ github.token }}\\n PR_NUMBER: ${{ github.event.pull_request.number }}\\n HEAD_SHA: ${{ github.event.pull_request.head.sha }}\\n VISUAL_RECAP_MODEL: ${{ vars.VISUAL_RECAP_MODEL }}\\n VISUAL_RECAP_REASONING: ${{ vars.VISUAL_RECAP_REASONING }}\\n VISUAL_RECAP_SKILL_SOURCE: ${{ vars.VISUAL_RECAP_SKILL_SOURCE || \\'auto\\' }}\\n steps:\\n - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3\\n with:\\n fetch-depth: 0\\n # This job runs an agent over untrusted PR diff; don\\'t leave the token\\n # in .git/config (it uses GH_TOKEN for gh API calls, never git push).\\n persist-credentials: false\\n\\n # Dogfood local source inside this monorepo, else the published package.\\n # The pnpm steps run ONLY on the local path so npm/yarn consumer repos\\n # (no pnpm-lock.yaml) fall back to `npx @agent-native/core`.\\n - name: Resolve recap CLI\\n id: cli\\n run: |\\n if [ -f packages/core/src/cli/index.ts ]; then\\n echo \"RECAP_CLI=pnpm exec tsx packages/core/src/cli/index.ts\" >> \"$GITHUB_ENV\"\\n echo \"local=true\" >> \"$GITHUB_OUTPUT\"\\n else\\n echo \"RECAP_CLI=npx -y @agent-native/core@latest\" >> \"$GITHUB_ENV\"\\n echo \"local=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8\\n if: steps.cli.outputs.local == \\'true\\'\\n\\n - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0\\n with:\\n node-version: \"22\"\\n cache: ${{ steps.cli.outputs.local == \\'true\\' && \\'pnpm\\' || \\'\\' }}\\n\\n - name: Install workspace (local source only)\\n if: steps.cli.outputs.local == \\'true\\'\\n run: pnpm install --frozen-lockfile --ignore-scripts\\n\\n - name: Start visual recap check\\n id: recap_check\\n continue-on-error: true\\n run: |\\n set -uo pipefail\\n $RECAP_CLI recap check start --sha \"$HEAD_SHA\" --workflow-url \"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID\"\\n\\n - name: Collect bounded diff\\n id: diff\\n env:\\n BASE_SHA: ${{ github.event.pull_request.base.sha }}\\n run: |\\n set -euo pipefail\\n $RECAP_CLI recap collect-diff --base \"$BASE_SHA\" --head \"$HEAD_SHA\" --out recap.diff --stat recap.stat\\n\\n - name: Secret scan\\n id: scan\\n if: steps.diff.outputs.tiny != \\'true\\'\\n run: |\\n set -uo pipefail\\n # Fail CLOSED: a scanner error or invalid JSON suppresses the diff so a\\n # credential-bearing diff is never handed to the agent / plan service.\\n if ! SCAN_JSON=\"$($RECAP_CLI recap scan --diff recap.diff)\"; then\\n SCAN_JSON=\\'{\"suppressed\":true,\"reason\":\"secret scan failed to run; failing closed\"}\\'\\n fi\\n {\\n echo \\'json<<__RECAP_SCAN_EOF__\\'\\n echo \"$SCAN_JSON\"\\n echo \\'__RECAP_SCAN_EOF__\\'\\n } >> \"$GITHUB_OUTPUT\"\\n SUPPRESSED=$(node -e \\'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?\"true\":\"false\")}catch{process.stdout.write(\"true\")}\\' \"$SCAN_JSON\")\\n echo \"suppressed=$SUPPRESSED\" >> \"$GITHUB_OUTPUT\"\\n\\n - name: Read previous plan id\\n id: prev\\n continue-on-error: true\\n run: |\\n set -euo pipefail\\n PLAN_ID=\"$($RECAP_CLI recap comment find-plan-id --repo \"$GITHUB_REPOSITORY\" --issue \"$PR_NUMBER\" --token \"$GH_TOKEN\")\"\\n echo \"plan_id=$PLAN_ID\" >> \"$GITHUB_OUTPUT\"\\n\\n - name: Build recap prompt\\n id: prompt\\n if: steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n env:\\n # Pass step outputs via env, NOT ${{ }} interpolation into the run body:\\n # the prev plan id is parsed from a PR comment and could inject shell.\\n PREV_PLAN_ID: ${{ steps.prev.outputs.plan_id }}\\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\\n run: |\\n set -euo pipefail\\n ARGS=(--diff recap.diff --stat recap.stat --pr \"$PR_NUMBER\" --repo \"$GITHUB_REPOSITORY\" --head \"$HEAD_SHA\" --app-url \"$PLAN_RECAP_APP_URL\" --skill-source \"$VISUAL_RECAP_SKILL_SOURCE\" --out recap-prompt.md)\\n if [ \"${DIFF_HUGE:-}\" = \"true\" ]; then ARGS+=(--huge); fi\\n if [ -n \"${PREV_PLAN_ID:-}\" ]; then ARGS+=(--prev-plan-id \"$PREV_PLAN_ID\"); fi\\n $RECAP_CLI recap build-prompt \"${ARGS[@]}\"\\n\\n - name: Run agent (Claude Code)\\n id: claude\\n if: needs.gate.outputs.agent == \\'claude\\' && steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n continue-on-error: true\\n env:\\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\\n run: |\\n set -uo pipefail\\n MCP_CONFIG=\"$RUNNER_TEMP/plan-mcp.json\"\\n $RECAP_CLI recap mcp-config --agent claude --app-url \"$PLAN_RECAP_APP_URL\" --out \"$MCP_CONFIG\"\\n CLAUDE_ARGS=(-p \"$(cat recap-prompt.md)\" --mcp-config \"$MCP_CONFIG\" --allowedTools \"Read,Write,Bash(git diff:*),mcp__plan__get-plan-blocks,mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility\" --permission-mode dontAsk --output-format json)\\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CLAUDE_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\\n npx -y @anthropic-ai/claude-code@2 \"${CLAUDE_ARGS[@]}\" > claude-result.json || true\\n rm -f \"$MCP_CONFIG\" || true\\n\\n - name: Run agent (Codex)\\n id: codex\\n if: needs.gate.outputs.agent == \\'codex\\' && steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n continue-on-error: true\\n env:\\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\\n run: |\\n set -uo pipefail\\n $RECAP_CLI recap mcp-config --agent codex --app-url \"$PLAN_RECAP_APP_URL\"\\n # `codex login` writes ~/.codex/auth.json (the bare env var is dropped on\\n # the gpt-5.5 wss transport); stdin keeps the key out of process args.\\n printenv OPENAI_API_KEY | npx -y @openai/codex@0 login --with-api-key || true\\n # The runner is itself an ephemeral sandbox; bypass Codex\\'s own sandbox\\n # (bubblewrap can\\'t init here) and approval gate (cancels the MCP write).\\n CODEX_ARGS=(exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check)\\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CODEX_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\\n # Validate reasoning against the enum before embedding it in the TOML override.\\n case \"${VISUAL_RECAP_REASONING:-}\" in\\n none|minimal|low|medium|high|xhigh)\\n CODEX_ARGS+=(-c \"model_reasoning_effort=\\\\\"$VISUAL_RECAP_REASONING\\\\\"\") ;;\\n \"\") ;;\\n *) echo \"Ignoring invalid VISUAL_RECAP_REASONING: $VISUAL_RECAP_REASONING\" ;;\\n esac\\n npx -y @openai/codex@0 \"${CODEX_ARGS[@]}\" --json \"$(cat recap-prompt.md)\" | tee codex-events.jsonl || true\\n\\n - name: Read plan URL\\n id: url\\n if: steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n run: |\\n set -uo pipefail\\n PLAN_URL=\"\"\\n if [ -f recap-url.txt ]; then PLAN_URL=\"$(tr -d \\'\\\\r\\\\n\\' < recap-url.txt | tr -d \\' \\')\"; fi\\n # recap-url.txt is agent-written -> untrusted. Rebuild a canonical\\n # recap URL from the trusted app base and a strictly validated plan id,\\n # preserving path-prefixed self-hosted mounts.\\n CANONICAL_URL=$(PLAN_URL=\"$PLAN_URL\" node <<\\'NODE\\'\\n try {\\n const raw = process.env.PLAN_URL || \"\";\\n const trusted = new URL(process.env.PLAN_RECAP_APP_URL || \"https://plan.agent-native.com\");\\n const parsed = /^https?:\\\\/\\\\//i.test(raw)\\n ? new URL(raw)\\n : new URL(raw, trusted);\\n if (parsed.origin !== trusted.origin) {\\n process.exit(0);\\n }\\n\\n const base = trusted.pathname.replace(/\\\\/$/, \"\");\\n const paths = [parsed.pathname];\\n if (base && parsed.pathname.startsWith(`${base}/`)) {\\n paths.push(parsed.pathname.slice(base.length) || \"/\");\\n }\\n\\n for (const path of paths) {\\n const match = path.match(/^\\\\/(?:plans|recaps)\\\\/([A-Za-z0-9_-]+)\\\\/?$/);\\n if (match) {\\n process.stdout.write(`${trusted.origin}${base}/recaps/${match[1]}`);\\n break;\\n }\\n }\\n } catch {\\n process.exit(0);\\n }\\n NODE\\n )\\n if [ -n \"$CANONICAL_URL\" ]; then\\n echo \"plan_url=$CANONICAL_URL\" >> \"$GITHUB_OUTPUT\"; echo \"ok=true\" >> \"$GITHUB_OUTPUT\"\\n else\\n echo \"plan_url=\" >> \"$GITHUB_OUTPUT\"; echo \"ok=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n - name: Attach usage\\n if: steps.url.outputs.ok == \\'true\\'\\n continue-on-error: true\\n env:\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n # Use the gate-normalized agent so \"Codex\" still selects the right file.\\n RECAP_AGENT: ${{ needs.gate.outputs.agent }}\\n run: |\\n set -uo pipefail\\n RESULT=claude-result.json\\n if [ \"$RECAP_AGENT\" = \"codex\" ]; then RESULT=codex-events.jsonl; fi\\n if [ -f \"$RESULT\" ]; then $RECAP_CLI recap usage --plan-url \"$PLAN_URL\" --agent \"$RECAP_AGENT\" --result-file \"$RESULT\" --model \"${VISUAL_RECAP_MODEL:-}\" --app-url \"$PLAN_RECAP_APP_URL\" --token \"$PLAN_RECAP_TOKEN\" || true; fi\\n\\n - name: Cache Playwright browsers\\n if: steps.url.outputs.ok == \\'true\\'\\n uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3\\n with:\\n path: ~/.cache/ms-playwright\\n key: playwright-1-${{ runner.os }}\\n\\n - name: Screenshot + upload\\n id: shot\\n if: steps.url.outputs.ok == \\'true\\'\\n continue-on-error: true\\n env:\\n # recap-url.txt is untrusted agent output; pass via env, never ${{ }}.\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n run: |\\n set -uo pipefail\\n pnpm exec playwright install --with-deps chromium 2>/dev/null || npx -y playwright@1 install --with-deps chromium || true\\n SHOT_JSON=\"$($RECAP_CLI recap shot --url \"$PLAN_URL\" --token \"$PLAN_RECAP_TOKEN\" --app-url \"$PLAN_RECAP_APP_URL\" --out recap.png || echo \\'{}\\')\"\\n IMAGE_URL=$(node -e \\'try{process.stdout.write(JSON.parse(process.argv[1]).imageUrl||\"\")}catch{process.stdout.write(\"\")}\\' \"$SHOT_JSON\")\\n echo \"image_url=$IMAGE_URL\" >> \"$GITHUB_OUTPUT\"\\n if [ -f recap.png ]; then echo \"captured=true\" >> \"$GITHUB_OUTPUT\"; else echo \"captured=false\" >> \"$GITHUB_OUTPUT\"; fi\\n\\n - name: Upload recap screenshot artifact\\n if: steps.shot.outputs.captured == \\'true\\'\\n uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1\\n with:\\n name: pr-visual-recap-${{ github.event.pull_request.number }}\\n path: recap.png\\n if-no-files-found: ignore\\n retention-days: 14\\n\\n - name: Upsert sticky comment\\n if: always()\\n continue-on-error: true\\n env:\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n RECAP_IMAGE_URL: ${{ steps.shot.outputs.image_url }}\\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\\n PREV_PLAN_ID: ${{ steps.prev.outputs.plan_id }}\\n run: |\\n set -euo pipefail\\n ARGS=(recap comment upsert --repo \"$GITHUB_REPOSITORY\" --issue \"$PR_NUMBER\" --token \"$GH_TOKEN\")\\n # On a tiny diff, only REFRESH an existing comment, never create one.\\n if [ \"${DIFF_TINY:-}\" = \"true\" ]; then ARGS+=(--update-only); fi\\n $RECAP_CLI \"${ARGS[@]}\"\\n\\n - name: Complete visual recap check\\n if: always() && steps.recap_check.outputs.check_run_id != \\'\\'\\n continue-on-error: true\\n env:\\n # Untrusted/step values via env (NOT ${{ }}-interpolated into the run\\n # body): the agent-written plan URL and the scan JSON could inject shell.\\n CHECK_RUN_ID: ${{ steps.recap_check.outputs.check_run_id }}\\n PLAN_OK: ${{ steps.url.outputs.ok }}\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\\n run: |\\n set -uo pipefail\\n $RECAP_CLI recap check complete \\\\\\n --check-run-id \"$CHECK_RUN_ID\" \\\\\\n --plan-ok \"$PLAN_OK\" \\\\\\n --plan-url \"$PLAN_URL\" \\\\\\n --suppressed \"$SUPPRESSED\" \\\\\\n --suppressed-json \"$SUPPRESSED_JSON\" \\\\\\n --huge \"$DIFF_HUGE\" \\\\\\n --tiny \"$DIFF_TINY\" \\\\\\n --workflow-url \"$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID\"\\n';\n"]}
@@ -1,6 +1,6 @@
1
1
  /**
2
- * `agent-native recap <scan|build-prompt|shot|comment>` — the helper surface
3
- * used by the PR Visual Recap GitHub Action.
2
+ * `agent-native recap` — the helper surface used by the PR Visual Recap GitHub
3
+ * Action. Run `agent-native recap help` for the full subcommand list.
4
4
  *
5
5
  * The action no longer generates the recap deterministically. Instead a coding
6
6
  * agent (Claude Code or Codex) RUNS THE REPO'S visual-recap skill against the
@@ -17,10 +17,15 @@
17
17
  * mcp-config Write the plan MCP client config for the chosen backend
18
18
  * (Claude Code JSON or Codex config.toml).
19
19
  * scan Refuse to hand a secret-leaking diff to the agent.
20
- * build-prompt Assemble the agent prompt = repo SKILL.md + a task wrapper.
20
+ * build-prompt Assemble the agent prompt = latest visual-recap skill bundle
21
+ * + a task wrapper (or repo-pinned skill with --skill-source).
21
22
  * shot Screenshot the published plan and upload it to the plan app's
22
23
  * signed public image route (for an inline PR-comment image).
24
+ * usage Parse and emit agent token-usage/cost from stdout.
23
25
  * comment Find the previous plan id / upsert the sticky PR comment.
26
+ * check Evaluate the recap result and set a GitHub commit status.
27
+ * setup Install the PR Visual Recap GitHub Action workflow.
28
+ * doctor Diagnose missing secrets / misconfigured workflow.
24
29
  *
25
30
  * Promoting these to the published CLI means an installed repo's workflow calls
26
31
  * `agent-native recap …` instead of copying helper scripts into the repo.
@@ -34,6 +39,26 @@ export declare function writePrVisualRecapWorkflow(baseDir: string): {
34
39
  path: string;
35
40
  existed: boolean;
36
41
  };
42
+ export type RecapAgent = "claude" | "codex";
43
+ export declare function normalizeRecapAgent(value: string | undefined): RecapAgent;
44
+ export declare function recapRequiredSecrets(agent: RecapAgent): string[];
45
+ export interface RecapSetupPlan {
46
+ agent: RecapAgent;
47
+ appUrl: string;
48
+ repo?: string;
49
+ workflowPath: string;
50
+ workflowExists: boolean;
51
+ requiredSecrets: string[];
52
+ variableValues: Record<string, string>;
53
+ secretValues: Record<string, string | undefined>;
54
+ }
55
+ export declare function buildRecapSetupPlan(input: {
56
+ baseDir: string;
57
+ appUrl?: string;
58
+ agent?: string;
59
+ repo?: string;
60
+ env?: NodeJS.ProcessEnv;
61
+ }): RecapSetupPlan;
37
62
  export declare function lineLooksSecret(line: string): boolean;
38
63
  export declare function diffContainsSecret(diffText: string): boolean;
39
64
  /** ~600KB byte cap for the diff handed to the recap agent. */
@@ -67,7 +92,12 @@ export declare function classifyDiff(input: {
67
92
  * agent's input. Pure (string in, string out) so it can be unit-tested.
68
93
  */
69
94
  export declare function truncateDiffAtLineBoundary(text: string): string;
70
- /** Count lines that begin with `+` or `-` (added/removed diff lines). */
95
+ /**
96
+ * Count lines that begin with `+` or `-` (added/removed diff lines), excluding
97
+ * the `+++ b/file` / `--- a/file` unified-diff header lines. Without this
98
+ * exclusion a single-file change loses ~2 "real" lines from the 8-line tiny
99
+ * threshold, incorrectly classifying a small-but-meaningful change as tiny.
100
+ */
71
101
  export declare function countDiffLines(diffText: string): number;
72
102
  /**
73
103
  * The Claude Code MCP config the recap agent loads: a single HTTP `plan` server
@@ -91,6 +121,11 @@ export declare function readRepoSkillMd(cwd?: string): {
91
121
  text: string;
92
122
  source: string;
93
123
  };
124
+ type RecapSkillSourceMode = "auto" | "latest" | "repo";
125
+ export declare function readVisualRecapSkillBundle(cwd?: string, mode?: RecapSkillSourceMode): {
126
+ text: string;
127
+ source: string;
128
+ };
94
129
  export declare function buildRecapPrompt(input: {
95
130
  skillMd: string;
96
131
  pr: string;
@@ -106,6 +141,23 @@ export declare function buildRecapPrompt(input: {
106
141
  }): string;
107
142
  /** Build the sticky comment body from the workflow's environment. */
108
143
  export declare function buildCommentBody(env?: NodeJS.ProcessEnv): string;
144
+ /**
145
+ * Confirm GitHub can fetch the uploaded image anonymously before we embed it.
146
+ *
147
+ * Default budget: 8 attempts with capped exponential backoff (1s, 2s, 3s, …
148
+ * capped at 4s) → ~20s total. This is enough to survive a cold-start CDN
149
+ * propagation delay that would otherwise cause `uploadRecapImage` to return a
150
+ * URL that the GitHub PR comment can't display.
151
+ *
152
+ * The `attempts` and `delayMs` overrides remain for unit tests and for callers
153
+ * that need a tighter or looser budget.
154
+ */
155
+ export declare function waitForPublicRecapImage(input: {
156
+ imageUrl: string;
157
+ attempts?: number;
158
+ delayMs?: number;
159
+ fetchFn?: typeof fetch;
160
+ }): Promise<boolean>;
109
161
  /**
110
162
  * Minimal shape of the `pull_request` object from a GitHub `pull_request` event
111
163
  * payload that the gate inspects. Everything is optional so a malformed/partial
@@ -147,8 +199,14 @@ export interface RecapGateInput {
147
199
  * recap job runs (the workflow itself, the skill, the local CLI, or any agent
148
200
  * config the runner loads) — so the whole job is skipped, not just the agent
149
201
  * step, to keep untrusted PR code away from the publish/API secrets.
202
+ *
203
+ * The `packages/core/**` rule is scoped to the BuilderIO/agent-native monorepo
204
+ * (where packages/core IS the recap CLI source) so that consumer repos with an
205
+ * unrelated `packages/core/` directory are not silently gated. Pass the
206
+ * `repository` ("owner/name") to apply that scoping; omit it to match the old
207
+ * unconditional behaviour (safe for the gate's self-test).
150
208
  */
151
- export declare function isRecapSensitivePath(p: string): boolean;
209
+ export declare function isRecapSensitivePath(p: string, repository?: string): boolean;
152
210
  /**
153
211
  * The pure gate decision: given the PR payload, secret-presence flags, the
154
212
  * configured backend/model, and the PR's changed files, decide whether the
@@ -1 +1 @@
1
- {"version":3,"file":"recap.d.ts","sourceRoot":"","sources":["../../src/cli/recap.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"recap.d.ts","sourceRoot":"","sources":["../../src/cli/recap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAsDH,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,EAAE,MAAM,EASzC,CAAC;AAEF,+DAA+D;AAC/D,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;CAClB,CAOA;AAED,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,OAAO,CAAC;AAI5C,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAOzE;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE,CAKhE;AAuJD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,OAAO,CAAC;IACxB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;CAClD;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,GAAG,cAAc,CAsCjB;AA+MD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa5D;AAMD,8DAA8D;AAC9D,eAAO,MAAM,mBAAmB,SAAS,CAAC;AAE1C,oEAAoE;AACpE,eAAO,MAAM,2BAA2B,wDACe,CAAC;AAexD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;CACvB,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAKnC;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAU/D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAOvD;AAuHD;;;;GAIG;AACH,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,MAAM,CAWR;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAS/D;AAmGD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAsB,GAAG;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAgBA;AAED,KAAK,oBAAoB,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAkEvD,wBAAgB,0BAA0B,CACxC,GAAG,GAAE,MAAsB,EAC3B,IAAI,GAAE,oBAA6B,GAClC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAKlC;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,CAiGT;AAkJD,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAiH7E;AA4DD;;;;;;;;;;GAUG;AACH,wBAAsB,uBAAuB,CAAC,KAAK,EAAE;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;CACxB,GAAG,OAAO,CAAC,OAAO,CAAC,CA0BnB;AAyND;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE;YAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;SAAE,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9D,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CAC/D;AAED,MAAM,WAAW,cAAc;IAC7B,8DAA8D;IAC9D,EAAE,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAChC,wCAAwC;IACxC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAC/B,gCAAgC;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,iCAAiC;IACjC,YAAY,EAAE,OAAO,CAAC;IACtB,8BAA8B;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,uDAAuD;IACvD,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,kEAAkE;IAClE,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAmB5E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG;IACxD,GAAG,EAAE,OAAO,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAwEA;AAuID;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAexE;AAED,+EAA+E;AAC/E,MAAM,WAAW,sBAAsB;IACrC,gFAAgF;IAChF,MAAM,EAAE,OAAO,CAAC;IAChB,2EAA2E;IAC3E,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,MAAM,EAAE,MAAM,CAAC;IACf,6EAA6E;IAC7E,IAAI,EAAE,OAAO,CAAC;IACd,iEAAiE;IACjE,IAAI,EAAE,OAAO,CAAC;IACd,6EAA6E;IAC7E,UAAU,EAAE,OAAO,CAAC;IACpB,iFAAiF;IACjF,cAAc,EAAE,MAAM,CAAC;IACvB,4DAA4D;IAC5D,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,SAAS,GAAG,SAAS,GAAG,SAAS,CAAC;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,sBAAsB,GAC5B,iBAAiB,CA4CnB;AAqJD,UAAU,WAAW;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,gBAAgB,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAwBD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAmBnE;AA2BD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAajE;AAsID,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA+C5D"}