@agent-native/core 0.42.0 → 0.44.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/README.md +17 -56
- package/dist/chat-threads/store.d.ts.map +1 -1
- package/dist/chat-threads/store.js +71 -10
- package/dist/chat-threads/store.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 +13 -13
- package/dist/cli/recap.js.map +1 -1
- package/dist/cli/skills.d.ts +2 -6
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +21 -79
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +76 -18
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/blocks/index.d.ts +9 -0
- package/dist/client/blocks/index.d.ts.map +1 -1
- package/dist/client/blocks/index.js +9 -0
- package/dist/client/blocks/index.js.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.js +3 -3
- 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 +1 -1
- package/dist/client/blocks/library/ApiEndpointBlock.js.map +1 -1
- package/dist/client/blocks/library/DiffBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/DiffBlock.js +128 -19
- package/dist/client/blocks/library/DiffBlock.js.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.js +31 -4
- package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
- package/dist/client/blocks/library/JsonExplorerBlock.js +1 -1
- package/dist/client/blocks/library/JsonExplorerBlock.js.map +1 -1
- package/dist/client/blocks/library/MermaidBlock.js +1 -1
- package/dist/client/blocks/library/MermaidBlock.js.map +1 -1
- package/dist/client/blocks/library/callout.config.d.ts +29 -0
- package/dist/client/blocks/library/callout.config.d.ts.map +1 -0
- package/dist/client/blocks/library/callout.config.js +33 -0
- package/dist/client/blocks/library/callout.config.js.map +1 -0
- package/dist/client/blocks/library/callout.d.ts +20 -0
- package/dist/client/blocks/library/callout.d.ts.map +1 -0
- package/dist/client/blocks/library/callout.js +61 -0
- package/dist/client/blocks/library/callout.js.map +1 -0
- package/dist/client/blocks/library/checklist.d.ts.map +1 -1
- package/dist/client/blocks/library/checklist.js +3 -3
- package/dist/client/blocks/library/checklist.js.map +1 -1
- package/dist/client/blocks/library/code-tabs.js +1 -1
- package/dist/client/blocks/library/code-tabs.js.map +1 -1
- package/dist/client/blocks/library/diagram.config.d.ts +64 -0
- package/dist/client/blocks/library/diagram.config.d.ts.map +1 -0
- package/dist/client/blocks/library/diagram.config.js +111 -0
- package/dist/client/blocks/library/diagram.config.js.map +1 -0
- package/dist/client/blocks/library/diagram.d.ts +16 -0
- package/dist/client/blocks/library/diagram.d.ts.map +1 -0
- package/dist/client/blocks/library/diagram.js +261 -0
- package/dist/client/blocks/library/diagram.js.map +1 -0
- package/dist/client/blocks/library/question-form.config.d.ts +69 -0
- package/dist/client/blocks/library/question-form.config.d.ts.map +1 -0
- package/dist/client/blocks/library/question-form.config.js +58 -0
- package/dist/client/blocks/library/question-form.config.js.map +1 -0
- package/dist/client/blocks/library/question-form.d.ts +20 -0
- package/dist/client/blocks/library/question-form.d.ts.map +1 -0
- package/dist/client/blocks/library/question-form.js +286 -0
- package/dist/client/blocks/library/question-form.js.map +1 -0
- package/dist/client/blocks/library/sanitize-html.d.ts +5 -0
- package/dist/client/blocks/library/sanitize-html.d.ts.map +1 -0
- package/dist/client/blocks/library/sanitize-html.js +240 -0
- package/dist/client/blocks/library/sanitize-html.js.map +1 -0
- package/dist/client/blocks/library/server-specs.d.ts.map +1 -1
- package/dist/client/blocks/library/server-specs.js +49 -0
- package/dist/client/blocks/library/server-specs.js.map +1 -1
- package/dist/client/blocks/library/specs.d.ts.map +1 -1
- package/dist/client/blocks/library/specs.js +9 -0
- package/dist/client/blocks/library/specs.js.map +1 -1
- package/dist/client/blocks/library/tabs.d.ts.map +1 -1
- package/dist/client/blocks/library/tabs.js +12 -12
- package/dist/client/blocks/library/tabs.js.map +1 -1
- package/dist/client/blocks/library/wireframe-kit.d.ts +260 -0
- package/dist/client/blocks/library/wireframe-kit.d.ts.map +1 -0
- package/dist/client/blocks/library/wireframe-kit.js +920 -0
- package/dist/client/blocks/library/wireframe-kit.js.map +1 -0
- package/dist/client/blocks/library/wireframe.config.d.ts +123 -0
- package/dist/client/blocks/library/wireframe.config.d.ts.map +1 -0
- package/dist/client/blocks/library/wireframe.config.js +311 -0
- package/dist/client/blocks/library/wireframe.config.js.map +1 -0
- package/dist/client/blocks/library/wireframe.d.ts +15 -0
- package/dist/client/blocks/library/wireframe.d.ts.map +1 -0
- package/dist/client/blocks/library/wireframe.js +206 -0
- package/dist/client/blocks/library/wireframe.js.map +1 -0
- package/dist/client/blocks/mdx.d.ts.map +1 -1
- package/dist/client/blocks/mdx.js +11 -0
- package/dist/client/blocks/mdx.js.map +1 -1
- package/dist/client/blocks/registry.d.ts +9 -0
- package/dist/client/blocks/registry.d.ts.map +1 -1
- package/dist/client/blocks/registry.js +12 -5
- package/dist/client/blocks/registry.js.map +1 -1
- package/dist/client/blocks/server.d.ts +1 -0
- package/dist/client/blocks/server.d.ts.map +1 -1
- package/dist/client/blocks/server.js +1 -0
- package/dist/client/blocks/server.js.map +1 -1
- package/dist/client/blocks/types.d.ts +8 -0
- 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 +112 -84
- package/dist/client/rich-markdown-editor/DragHandle.js.map +1 -1
- package/dist/client/rich-markdown-editor/RegistryBlockNode.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/RegistryBlockNode.js +1 -1
- package/dist/client/rich-markdown-editor/RegistryBlockNode.js.map +1 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts +9 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.js +3 -1
- package/dist/client/rich-markdown-editor/SharedRichEditor.js.map +1 -1
- package/dist/client/rich-markdown-editor/extensions.d.ts +13 -1
- package/dist/client/rich-markdown-editor/extensions.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/extensions.js +4 -2
- package/dist/client/rich-markdown-editor/extensions.js.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.d.ts.map +1 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.js +11 -1
- package/dist/client/rich-markdown-editor/useCollabReconcile.js.map +1 -1
- package/dist/server/poll.d.ts.map +1 -1
- package/dist/server/poll.js +30 -14
- package/dist/server/poll.js.map +1 -1
- package/dist/styles/agent-native.css +1 -0
- package/dist/styles/blocks.css +1388 -0
- package/dist/templates/default/.agents/skills/storing-data/SKILL.md +2 -0
- package/dist/templates/workspace-core/.agents/skills/performance/SKILL.md +141 -0
- package/dist/templates/workspace-core/.agents/skills/storing-data/SKILL.md +2 -0
- package/docs/content/plan-plugin.md +8 -8
- package/docs/content/pr-visual-recap.md +2 -2
- package/docs/content/template-plan.md +94 -17
- package/package.json +2 -1
- package/src/templates/default/.agents/skills/storing-data/SKILL.md +2 -0
- package/src/templates/workspace-core/.agents/skills/performance/SKILL.md +141 -0
- package/src/templates/workspace-core/.agents/skills/storing-data/SKILL.md +2 -0
- package/docs/content/visual-plans.md +0 -82
|
@@ -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 # 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';
|
|
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 agent: ${{ steps.decide.outputs.agent }}\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. Normalize the agent\n // value once here and validate it: an unknown or mis-cased value\n // (e.g. "Claude", "gpt") must NOT silently pass the gate and then\n // match neither agent step below.\n const agent = (process.env.AGENT || \'claude\').toLowerCase();\n if (agent !== \'claude\' && agent !== \'codex\') {\n reasons.push(`unsupported VISUAL_RECAP_AGENT "${process.env.AGENT}" (expected "claude" or "codex")`);\n } else if (agent === \'codex\') {\n if (process.env.HAS_OPENAI !== \'true\') reasons.push(\'OPENAI_API_KEY not configured (codex backend)\');\n } else {\n if (process.env.HAS_ANTHROPIC !== \'true\') reasons.push(\'ANTHROPIC_API_KEY not configured (claude backend)\');\n }\n\n // Self-modifying guard, evaluated in the GATE (trusted github-script\n // that runs NO PR-checked-out code): if this PR changes the workflow,\n // the visual-recap/visual-plan skill, the local CLI (packages/core),\n // or any agent config the runner would load (.claude/**, CLAUDE.md,\n // .mcp.json), skip the ENTIRE job — not just the agent — so a PR can\n // never rewrite what runs (skill, hooks, settings, CLI) and exfiltrate\n // the publish/API secrets.\n if (pr) {\n try {\n const files = await github.paginate(github.rest.pulls.listFiles, {\n owner: context.repo.owner,\n repo: context.repo.repo,\n pull_number: pr.number,\n per_page: 100,\n });\n const isSensitive = (p) =>\n p === \'.github/workflows/pr-visual-recap.yml\' ||\n /(^|\\/)skills\\/visual-(recap|plan|plans)\\//.test(p) ||\n /(^|\\/)\\.claude\\//.test(p) ||\n /(^|\\/)CLAUDE\\.md$/.test(p) ||\n /(^|\\/)AGENTS\\.md$/.test(p) ||\n /(^|\\/)\\.mcp\\.json$/.test(p) ||\n /(^|\\/)packages\\/core\\//.test(p);\n const hits = files.map((f) => f.filename).filter(isSensitive);\n if (hits.length) {\n reasons.push(`PR modifies recap-control files (${hits.slice(0, 3).join(\', \')}${hits.length > 3 ? \', …\' : \'\'}) — skipping so untrusted PR code never runs with secrets`);\n }\n } catch (e) {\n reasons.push(`could not list PR files for the self-modifying guard (${e.message}); skipping to be safe`);\n }\n }\n\n const run = reasons.length === 0;\n core.setOutput(\'run\', run ? \'true\' : \'false\');\n // Export the NORMALIZED agent so the recap job\'s step conditions match\n // case-insensitively via needs.gate.outputs.agent.\n core.setOutput(\'agent\', agent);\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 --ignore-scripts\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. Over the cap we both flag `huge` (so the agent is told to\n # summarize) AND physically truncate recap.diff, so an oversized diff\n # cannot overflow the agent\'s prompt budget when it reads the file.\n # Truncate at a COMPLETE LINE boundary (`sed \'$d\'` drops the last,\n # possibly-partial, line) so the byte cap can never cut a multi-byte\n # UTF-8 char or a diff line mid-way and corrupt the agent\'s input.\n if [ "$BYTES" -gt 614400 ]; then\n echo "huge=true" >> "$GITHUB_OUTPUT"\n head -c 614400 recap.diff | sed \'$d\' > recap.diff.capped && mv recap.diff.capped recap.diff\n printf \'\\n\\n[diff truncated at 600KB for the recap agent]\\n\' >> recap.diff\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 # Fail CLOSED: if the scanner errors or emits invalid JSON, treat the\n # diff as suppressed, so a scan failure can never hand a possibly\n # credential-bearing diff to the agent / plan service.\n if ! SCAN_JSON="$($RECAP_CLI recap scan --diff recap.diff)"; then\n SCAN_JSON=\'{"suppressed":true,"reason":"secret scan failed to run; failing closed"}\'\n fi\n # Multi-line-safe write: SCAN_JSON could contain newlines, which would\n # otherwise corrupt $GITHUB_OUTPUT or inject extra keys.\n {\n echo \'json<<__RECAP_SCAN_EOF__\'\n echo "$SCAN_JSON"\n echo \'__RECAP_SCAN_EOF__\'\n } >> "$GITHUB_OUTPUT"\n SUPPRESSED=$(node -e \'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?"true":"false")}catch{process.stdout.write("true")}\' "$SCAN_JSON")\n echo "suppressed=$SUPPRESSED" >> "$GITHUB_OUTPUT"\n\n # 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: needs.gate.outputs.agent == \'claude\' && steps.diff.outputs.tiny != \'true\' && steps.scan.outputs.suppressed != \'true\'\n continue-on-error: true\n env:\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\n run: |\n set -uo pipefail\n MCP_CONFIG="$RUNNER_TEMP/plan-mcp.json"\n 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: needs.gate.outputs.agent == \'codex\' && steps.diff.outputs.tiny != \'true\' && steps.scan.outputs.suppressed != \'true\'\n continue-on-error: true\n env:\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\n run: |\n set -uo pipefail\n 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 # The recap is informational/non-blocking: a failed comment upsert must\n # not turn the whole job red (the agent + screenshot steps are already\n # continue-on-error).\n continue-on-error: true\n env:\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\n RECAP_IMAGE_URL: ${{ steps.shot.outputs.image_url }}\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\n run: |\n set -euo pipefail\n ARGS=(recap comment upsert --repo "$GITHUB_REPOSITORY" --issue "$PR_NUMBER" --token "$GH_TOKEN")\n # On a tiny diff, only REFRESH an existing recap comment — never create\n # a new one — so we add no noise but also can\'t leave a stale prior\n # 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,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"]}
|
|
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,42uBAA42uB,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 agent: ${{ steps.decide.outputs.agent }}\\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. Normalize the agent\\n // value once here and validate it: an unknown or mis-cased value\\n // (e.g. \"Claude\", \"gpt\") must NOT silently pass the gate and then\\n // match neither agent step below.\\n const agent = (process.env.AGENT || \\'claude\\').toLowerCase();\\n if (agent !== \\'claude\\' && agent !== \\'codex\\') {\\n reasons.push(`unsupported VISUAL_RECAP_AGENT \"${process.env.AGENT}\" (expected \"claude\" or \"codex\")`);\\n } else if (agent === \\'codex\\') {\\n if (process.env.HAS_OPENAI !== \\'true\\') reasons.push(\\'OPENAI_API_KEY not configured (codex backend)\\');\\n } else {\\n if (process.env.HAS_ANTHROPIC !== \\'true\\') reasons.push(\\'ANTHROPIC_API_KEY not configured (claude backend)\\');\\n }\\n\\n // Self-modifying guard, evaluated in the GATE (trusted github-script\\n // that runs NO PR-checked-out code): if this PR changes the workflow,\\n // the visual-recap/visual-plan skill, the local CLI (packages/core),\\n // or any agent config the runner would load (.claude/**, CLAUDE.md,\\n // .mcp.json), skip the ENTIRE job — not just the agent — so a PR can\\n // never rewrite what runs (skill, hooks, settings, CLI) and exfiltrate\\n // the publish/API secrets.\\n if (pr) {\\n try {\\n const files = await github.paginate(github.rest.pulls.listFiles, {\\n owner: context.repo.owner,\\n repo: context.repo.repo,\\n pull_number: pr.number,\\n per_page: 100,\\n });\\n const isSensitive = (p) =>\\n p === \\'.github/workflows/pr-visual-recap.yml\\' ||\\n /(^|\\\\/)skills\\\\/visual-(recap|plan|plans)\\\\//.test(p) ||\\n /(^|\\\\/)\\\\.claude\\\\//.test(p) ||\\n /(^|\\\\/)CLAUDE\\\\.md$/.test(p) ||\\n /(^|\\\\/)AGENTS\\\\.md$/.test(p) ||\\n /(^|\\\\/)\\\\.mcp\\\\.json$/.test(p) ||\\n /(^|\\\\/)packages\\\\/core\\\\//.test(p);\\n const hits = files.map((f) => f.filename).filter(isSensitive);\\n if (hits.length) {\\n reasons.push(`PR modifies recap-control files (${hits.slice(0, 3).join(\\', \\')}${hits.length > 3 ? \\', …\\' : \\'\\'}) — skipping so untrusted PR code never runs with secrets`);\\n }\\n } catch (e) {\\n reasons.push(`could not list PR files for the self-modifying guard (${e.message}); skipping to be safe`);\\n }\\n }\\n\\n const run = reasons.length === 0;\\n core.setOutput(\\'run\\', run ? \\'true\\' : \\'false\\');\\n // Export the NORMALIZED agent so the recap job\\'s step conditions match\\n // case-insensitively via needs.gate.outputs.agent.\\n core.setOutput(\\'agent\\', agent);\\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 --ignore-scripts\\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. Over the cap we both flag `huge` (so the agent is told to\\n # summarize) AND physically truncate recap.diff, so an oversized diff\\n # cannot overflow the agent\\'s prompt budget when it reads the file.\\n # Truncate at a COMPLETE LINE boundary (`sed \\'$d\\'` drops the last,\\n # possibly-partial, line) so the byte cap can never cut a multi-byte\\n # UTF-8 char or a diff line mid-way and corrupt the agent\\'s input.\\n if [ \"$BYTES\" -gt 614400 ]; then\\n echo \"huge=true\" >> \"$GITHUB_OUTPUT\"\\n head -c 614400 recap.diff | sed \\'$d\\' > recap.diff.capped && mv recap.diff.capped recap.diff\\n printf \\'\\\\n\\\\n[diff truncated at 600KB for the recap agent]\\\\n\\' >> recap.diff\\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 # Fail CLOSED: if the scanner errors or emits invalid JSON, treat the\\n # diff as suppressed, so a scan failure can never hand a possibly\\n # credential-bearing diff to the agent / plan service.\\n if ! SCAN_JSON=\"$($RECAP_CLI recap scan --diff recap.diff)\"; then\\n SCAN_JSON=\\'{\"suppressed\":true,\"reason\":\"secret scan failed to run; failing closed\"}\\'\\n fi\\n # Multi-line-safe write: SCAN_JSON could contain newlines, which would\\n # otherwise corrupt $GITHUB_OUTPUT or inject extra keys.\\n {\\n echo \\'json<<__RECAP_SCAN_EOF__\\'\\n echo \"$SCAN_JSON\"\\n echo \\'__RECAP_SCAN_EOF__\\'\\n } >> \"$GITHUB_OUTPUT\"\\n SUPPRESSED=$(node -e \\'try{process.stdout.write(JSON.parse(process.argv[1]).suppressed?\"true\":\"false\")}catch{process.stdout.write(\"true\")}\\' \"$SCAN_JSON\")\\n echo \"suppressed=$SUPPRESSED\" >> \"$GITHUB_OUTPUT\"\\n\\n # 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: needs.gate.outputs.agent == \\'claude\\' && steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n continue-on-error: true\\n env:\\n ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}\\n run: |\\n set -uo pipefail\\n MCP_CONFIG=\"$RUNNER_TEMP/plan-mcp.json\"\\n 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: needs.gate.outputs.agent == \\'codex\\' && steps.diff.outputs.tiny != \\'true\\' && steps.scan.outputs.suppressed != \\'true\\'\\n continue-on-error: true\\n env:\\n OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}\\n run: |\\n set -uo pipefail\\n 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 # The recap is informational/non-blocking: a failed comment upsert must\\n # not turn the whole job red (the agent + screenshot steps are already\\n # continue-on-error).\\n continue-on-error: true\\n env:\\n PLAN_URL: ${{ steps.url.outputs.plan_url }}\\n RECAP_IMAGE_URL: ${{ steps.shot.outputs.image_url }}\\n SUPPRESSED: ${{ steps.scan.outputs.suppressed }}\\n SUPPRESSED_JSON: ${{ steps.scan.outputs.json }}\\n DIFF_HUGE: ${{ steps.diff.outputs.huge }}\\n DIFF_TINY: ${{ steps.diff.outputs.tiny }}\\n run: |\\n set -euo pipefail\\n ARGS=(recap comment upsert --repo \"$GITHUB_REPOSITORY\" --issue \"$PR_NUMBER\" --token \"$GH_TOKEN\")\\n # On a tiny diff, only REFRESH an existing recap comment — never create\\n # a new one — so we add no noise but also can\\'t leave a stale prior\\n # 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;
|
|
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;AAgJD,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAwF7E;AAwPD,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA0B5D"}
|
package/dist/cli/recap.js
CHANGED
|
@@ -162,7 +162,7 @@ export function buildRecapPrompt(input) {
|
|
|
162
162
|
? `, passing \`planId: "${input.prevPlanId}"\` so this REPLACES the existing recap plan`
|
|
163
163
|
: ""}.`);
|
|
164
164
|
lines.push(`2. Call the **set-resource-visibility** tool on the \`plan\` MCP server with \`{ resourceType: "plan", resourceId: <the returned plan id>, visibility: "org" }\` so the recap is login-gated to the org, never public.`);
|
|
165
|
-
lines.push(`3. Write the plan URL to a file named \`recap-url.txt\` at the repo root, containing exactly one line: \`${appUrl}/
|
|
165
|
+
lines.push(`3. Write the plan URL to a file named \`recap-url.txt\` at the repo root, containing exactly one line: \`${appUrl}/recaps/<the returned plan id>\`. This file is the workflow's only hand-off — do not print anything else as the deliverable.`);
|
|
166
166
|
lines.push("");
|
|
167
167
|
lines.push("Do not invent file names, schema fields, or endpoints. Redact anything that looks like a secret. If the diff has no reviewable substance, still publish a minimal recap and write recap-url.txt.");
|
|
168
168
|
lines.push("");
|
|
@@ -240,7 +240,9 @@ async function upsertComment(input) {
|
|
|
240
240
|
return { action: "created", id: created.id, html_url: created.html_url };
|
|
241
241
|
}
|
|
242
242
|
function planIdFromUrl(url) {
|
|
243
|
-
|
|
243
|
+
// Accept both /recaps/<id> (the canonical recap route the agent now writes)
|
|
244
|
+
// and /plans/<id> (legacy URLs) so the sticky-comment rebuild keeps working.
|
|
245
|
+
const match = url.match(/\/(?:recaps|plans)\/([A-Za-z0-9_-]+)/);
|
|
244
246
|
return match ? match[1] : null;
|
|
245
247
|
}
|
|
246
248
|
/** True when both URLs parse and share an origin. */
|
|
@@ -264,7 +266,6 @@ function originOf(url) {
|
|
|
264
266
|
/** Build the sticky comment body from the workflow's environment. */
|
|
265
267
|
export function buildCommentBody(env = process.env) {
|
|
266
268
|
const headShort = (env.HEAD_SHA || "").slice(0, 7);
|
|
267
|
-
const aid = "_A visual recap is an aid, not a replacement for reviewing the diff._";
|
|
268
269
|
const lines = [MARKER];
|
|
269
270
|
if (env.SUPPRESSED === "true") {
|
|
270
271
|
let reason = "potential secret in diff";
|
|
@@ -281,8 +282,6 @@ export function buildCommentBody(env = process.env) {
|
|
|
281
282
|
lines.push("The recap was **suppressed** because the diff matched a secret/credential pattern. No plan was published.");
|
|
282
283
|
lines.push("");
|
|
283
284
|
lines.push(`Reason: \`${reason}\`. Updated for \`${headShort}\`.`);
|
|
284
|
-
lines.push("");
|
|
285
|
-
lines.push(aid);
|
|
286
285
|
return lines.join("\n");
|
|
287
286
|
}
|
|
288
287
|
// Tiny diffs aren't worth a recap. Refresh an existing sticky comment to this
|
|
@@ -294,8 +293,6 @@ export function buildCommentBody(env = process.env) {
|
|
|
294
293
|
lines.push("The change in this push is too small to be worth a visual recap. This is informational only and does **not** block the PR.");
|
|
295
294
|
lines.push("");
|
|
296
295
|
lines.push(`Updated for \`${headShort}\`.`);
|
|
297
|
-
lines.push("");
|
|
298
|
-
lines.push(aid);
|
|
299
296
|
return lines.join("\n");
|
|
300
297
|
}
|
|
301
298
|
const planUrl = (env.PLAN_URL || "").trim();
|
|
@@ -309,15 +306,13 @@ export function buildCommentBody(env = process.env) {
|
|
|
309
306
|
const planId = planUrl ? planIdFromUrl(planUrl) : null;
|
|
310
307
|
const sameOriginOk = appUrl === "" || sameOrigin(planUrl, appUrl);
|
|
311
308
|
const base = (appUrl || originOf(planUrl)).replace(/\/$/, "");
|
|
312
|
-
const safeUrl = planId && base && sameOriginOk ? `${base}/
|
|
309
|
+
const safeUrl = planId && base && sameOriginOk ? `${base}/recaps/${planId}` : "";
|
|
313
310
|
if (!safeUrl) {
|
|
314
311
|
lines.push("### Visual recap — generation failed");
|
|
315
312
|
lines.push("");
|
|
316
313
|
lines.push("The visual recap could not be generated for this push. This is informational only and does **not** block the PR.");
|
|
317
314
|
lines.push("");
|
|
318
315
|
lines.push(`Updated for \`${headShort}\`.`);
|
|
319
|
-
lines.push("");
|
|
320
|
-
lines.push(aid);
|
|
321
316
|
return lines.join("\n");
|
|
322
317
|
}
|
|
323
318
|
// The image URL is produced by our own recap-image route, but validate it is
|
|
@@ -341,7 +336,7 @@ export function buildCommentBody(env = process.env) {
|
|
|
341
336
|
lines.push("> Large diff — this recap is a **summarized** view (top files + schema/API deltas).");
|
|
342
337
|
}
|
|
343
338
|
lines.push("");
|
|
344
|
-
lines.push(`Updated for \`${headShort}
|
|
339
|
+
lines.push(`Updated for \`${headShort}\`.`);
|
|
345
340
|
lines.push("");
|
|
346
341
|
lines.push(`<!-- plan-id: ${planId} -->`);
|
|
347
342
|
return lines.join("\n");
|
|
@@ -461,7 +456,7 @@ async function runShot(args) {
|
|
|
461
456
|
try {
|
|
462
457
|
browser = await chromium.launch({ args: ["--no-sandbox"] });
|
|
463
458
|
const context = await browser.newContext({
|
|
464
|
-
viewport: { width:
|
|
459
|
+
viewport: { width: 1450, height: 1450 },
|
|
465
460
|
deviceScaleFactor: 2,
|
|
466
461
|
});
|
|
467
462
|
if (attachToken) {
|
|
@@ -503,7 +498,12 @@ async function runShot(args) {
|
|
|
503
498
|
}
|
|
504
499
|
}
|
|
505
500
|
await page.waitForTimeout(matched ? 1_200 : 500);
|
|
506
|
-
|
|
501
|
+
// Zoom out slightly so more content fits. Keep the plan title (h1) in frame:
|
|
502
|
+
// the recap reads better led by its own title than cropped to the body.
|
|
503
|
+
await page.evaluate(() => {
|
|
504
|
+
document.documentElement.style.zoom = "80%";
|
|
505
|
+
});
|
|
506
|
+
await page.screenshot({ path: out });
|
|
507
507
|
captured = true;
|
|
508
508
|
await browser.close();
|
|
509
509
|
}
|
package/dist/cli/recap.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recap.js","sourceRoot":"","sources":["../../src/cli/recap.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `agent-native recap <scan|build-prompt|shot|comment>` — the helper surface\n * used by the PR Visual Recap GitHub Action.\n *\n * The action no longer generates the recap deterministically. Instead a coding\n * agent (Claude Code or Codex) RUNS THE REPO'S visual-recap skill against the\n * diff and publishes the plan via the plan MCP tools. These subcommands are the\n * thin, deterministic glue around that:\n *\n * scan Refuse to hand a secret-leaking diff to the agent.\n * build-prompt Assemble the agent prompt = repo SKILL.md + a task wrapper.\n * shot Screenshot the published plan and upload it to the plan app's\n * signed public image route (for an inline PR-comment image).\n * comment Find the previous plan id / upsert the sticky PR comment.\n *\n * Promoting these to the published CLI means an installed repo's workflow calls\n * `agent-native recap …` instead of copying helper scripts into the repo.\n *\n * Node built-ins only (plus an optional dynamic `playwright` import for `shot`).\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { PR_VISUAL_RECAP_WORKFLOW_YML } from \"./pr-visual-recap-workflow.js\";\n\n/* -------------------------------------------------------------------------- */\n/* Arg parsing */\n/* -------------------------------------------------------------------------- */\n\nfunction parseArgs(argv: string[]): Record<string, string | boolean> {\n const out: Record<string, string | boolean> = {};\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (!token.startsWith(\"--\")) continue;\n const key = token.slice(2);\n const next = argv[i + 1];\n if (next === undefined || next.startsWith(\"--\")) out[key] = true;\n else {\n out[key] = next;\n i += 1;\n }\n }\n return out;\n}\n\nfunction stringArg(\n args: Record<string, string | boolean>,\n key: string,\n): string {\n const value = args[key];\n if (typeof value !== \"string\" || value.length === 0) {\n throw new Error(`Missing --${key}`);\n }\n return value;\n}\n\nfunction optionalArg(\n args: Record<string, string | boolean>,\n key: string,\n): string | undefined {\n const value = args[key];\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\n/* -------------------------------------------------------------------------- */\n/* GitHub Action install (used by `skills add … --with-github-action`) */\n/* -------------------------------------------------------------------------- */\n\n/** GitHub secrets the installed PR Visual Recap workflow needs. */\nexport const PR_VISUAL_RECAP_SETUP: string[] = [\n \"Required secrets:\",\n \" PLAN_RECAP_TOKEN — bearer token from `agent-native connect`\",\n \" ANTHROPIC_API_KEY — the LLM key for the default Claude Code backend\",\n \"Optional (only if you change defaults):\",\n \" OPENAI_API_KEY (secret) + VISUAL_RECAP_AGENT=codex (variable) — use Codex instead of Claude\",\n \" VISUAL_RECAP_MODEL / VISUAL_RECAP_REASONING (variables) — pin the model (e.g. gpt-5.5) and reasoning depth (none|minimal|low|medium|high|xhigh; Codex only)\",\n \" PLAN_RECAP_APP_URL (secret) — only when self-hosting the plan app (defaults to https://plan.agent-native.com)\",\n];\n\n/** Write .github/workflows/pr-visual-recap.yml into a repo. */\nexport function writePrVisualRecapWorkflow(baseDir: string): {\n path: string;\n existed: boolean;\n} {\n const dir = path.resolve(baseDir, \".github\", \"workflows\");\n fs.mkdirSync(dir, { recursive: true });\n const file = path.join(dir, \"pr-visual-recap.yml\");\n const existed = fs.existsSync(file);\n fs.writeFileSync(file, PR_VISUAL_RECAP_WORKFLOW_YML);\n return { path: path.relative(baseDir, file), existed };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Secret scan — defense-in-depth before any LLM sees the diff */\n/* -------------------------------------------------------------------------- */\n\n/**\n * If the diff contains anything that looks like a real secret, we refuse to\n * build a recap at all (rather than risk echoing it into a published plan).\n * These patterns intentionally err toward caution and scan added, removed, and\n * context lines so deleting a real secret does not leak it in a split diff.\n */\nconst SECRET_PATTERNS: RegExp[] = [\n // Common provider key prefixes.\n /\\b(?:sk|pk|rk)-[A-Za-z0-9]{16,}\\b/,\n /\\bghp_[A-Za-z0-9]{20,}\\b/,\n /\\bgithub_pat_[A-Za-z0-9_]{20,}\\b/,\n /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/,\n /\\bAKIA[0-9A-Z]{16}\\b/,\n /\\bAIza[0-9A-Za-z_-]{20,}\\b/,\n // Bearer / Authorization header values with an actual token.\n /authorization\\s*[:=]\\s*['\"]?bearer\\s+[A-Za-z0-9._-]{12,}/i,\n // Private key blocks.\n /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/,\n // `KEY=...`, `TOKEN=...`, `SECRET=...`, `PASSWORD=...` assigned a real-looking\n // value (long, non-placeholder).\n /\\b[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|API_KEY|PRIVATE_KEY|ACCESS_KEY)[A-Z0-9_]*\\s*[:=]\\s*['\"]?(?!.*(?:your|example|placeholder|changeme|xxxx|\\*\\*\\*|<|\\$\\{|process\\.env|env\\.|REDACTED))[A-Za-z0-9/_+=.-]{16,}/i,\n];\n\nexport function lineLooksSecret(line: string): boolean {\n return SECRET_PATTERNS.some((re) => re.test(line));\n}\n\nexport function diffContainsSecret(diffText: string): boolean {\n for (const line of diffText.split(\"\\n\")) {\n if (\n line.startsWith(\"+\") ||\n line.startsWith(\"-\") ||\n line.startsWith(\" \") ||\n line.startsWith(\"+++\") ||\n line.startsWith(\"---\")\n ) {\n if (lineLooksSecret(line)) return true;\n }\n }\n return false;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Prompt builder — repo SKILL.md + task wrapper */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Locate the repo's visual-recap SKILL.md, preferring the host-agent install\n * locations so a user's `agent-native skills add` copy wins, then falling back\n * to the framework's own source locations.\n */\nexport function readRepoSkillMd(cwd: string = process.cwd()): {\n text: string;\n source: string;\n} {\n const candidates = [\n \".claude/skills/visual-recap/SKILL.md\",\n \".agents/skills/visual-recap/SKILL.md\",\n \"skills/visual-recap/SKILL.md\",\n \"templates/plan/.agents/skills/visual-recap/SKILL.md\",\n ];\n for (const rel of candidates) {\n const abs = path.resolve(cwd, rel);\n if (fs.existsSync(abs)) {\n return { text: fs.readFileSync(abs, \"utf8\"), source: rel };\n }\n }\n throw new Error(\n \"Could not find visual-recap/SKILL.md. Run `agent-native skills add visual-plan` first.\",\n );\n}\n\nexport function buildRecapPrompt(input: {\n skillMd: string;\n pr: string;\n head?: string;\n appUrl: string;\n diffPath: string;\n statPath?: string;\n prevPlanId?: string;\n huge?: boolean;\n}): string {\n const appUrl = input.appUrl.replace(/\\/$/, \"\");\n const lines: string[] = [];\n lines.push(\"# Task: publish a Visual Recap of this pull request\");\n lines.push(\"\");\n lines.push(\n `You are running non-interactively in CI. Follow the **visual-recap skill** included verbatim below to turn this PR's diff into a grounded Agent-Native Plan, then publish it.`,\n );\n lines.push(\"\");\n lines.push(\"## Inputs (read them from disk with your Read tool)\");\n lines.push(`- PR number: **#${input.pr}**`);\n if (input.head) lines.push(`- Head commit: \\`${input.head}\\``);\n lines.push(`- Unified diff: \\`${input.diffPath}\\` (read this file)`);\n if (input.statPath)\n lines.push(`- Diff stat: \\`${input.statPath}\\` (read this file)`);\n if (input.huge) {\n lines.push(\n `- The diff is LARGE — produce a **summarized** recap (top files + schema/API deltas), not an exhaustive one.`,\n );\n }\n lines.push(\"\");\n lines.push(\"## Publish (this is the only way to produce output)\");\n lines.push(\n `The \\`plan\\` MCP server is configured for you. Call its tools by name (your host may expose them as \\`create-visual-recap\\` or \\`mcp__plan__create-visual-recap\\` — same tool).`,\n );\n lines.push(\n `1. Call the **create-visual-recap** tool on the \\`plan\\` MCP server with grounded MDX derived ONLY from the real diff${\n input.prevPlanId\n ? `, passing \\`planId: \"${input.prevPlanId}\"\\` so this REPLACES the existing recap plan`\n : \"\"\n }.`,\n );\n lines.push(\n `2. Call the **set-resource-visibility** tool on the \\`plan\\` MCP server with \\`{ resourceType: \"plan\", resourceId: <the returned plan id>, visibility: \"org\" }\\` so the recap is login-gated to the org, never public.`,\n );\n lines.push(\n `3. Write the plan URL to a file named \\`recap-url.txt\\` at the repo root, containing exactly one line: \\`${appUrl}/plans/<the returned plan id>\\`. This file is the workflow's only hand-off — do not print anything else as the deliverable.`,\n );\n lines.push(\"\");\n lines.push(\n \"Do not invent file names, schema fields, or endpoints. Redact anything that looks like a secret. If the diff has no reviewable substance, still publish a minimal recap and write recap-url.txt.\",\n );\n lines.push(\"\");\n lines.push(\"---\");\n lines.push(\"\");\n lines.push(\"# visual-recap skill (follow this exactly)\");\n lines.push(\"\");\n lines.push(input.skillMd.trim());\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\n/* -------------------------------------------------------------------------- */\n/* GitHub comment helpers */\n/* -------------------------------------------------------------------------- */\n\nconst MARKER = \"<!-- pr-visual-recap -->\";\n\ntype GitHubComment = {\n id: number;\n body?: string | null;\n html_url?: string;\n user?: { type?: string | null } | null;\n};\n\nfunction repoParts(repoFullName: string): { owner: string; repo: string } {\n const [owner, repo] = repoFullName.split(\"/\");\n if (!owner || !repo) throw new Error(`Invalid --repo: ${repoFullName}`);\n return { owner, repo };\n}\n\nasync function githubRequest<T>(\n token: string,\n apiPath: string,\n init: RequestInit = {},\n): Promise<T> {\n const res = await fetch(`https://api.github.com${apiPath}`, {\n ...init,\n headers: {\n accept: \"application/vnd.github+json\",\n authorization: `Bearer ${token}`,\n \"x-github-api-version\": \"2022-11-28\",\n ...(init.headers ?? {}),\n },\n });\n if (!res.ok) {\n const detail = await res.text().catch(() => \"\");\n throw new Error(\n `GitHub request failed ${res.status} ${res.statusText}: ${detail.slice(0, 500)}`,\n );\n }\n if (res.status === 204) return undefined as T;\n return (await res.json()) as T;\n}\n\nasync function findExistingComment(input: {\n token: string;\n owner: string;\n repo: string;\n issue: string;\n}): Promise<GitHubComment | null> {\n for (let page = 1; ; page += 1) {\n const comments = await githubRequest<GitHubComment[]>(\n input.token,\n `/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(\n input.repo,\n )}/issues/${encodeURIComponent(input.issue)}/comments?per_page=100&page=${page}`,\n );\n const match = comments.find(\n (comment) =>\n comment.user?.type === \"Bot\" &&\n typeof comment.body === \"string\" &&\n comment.body.includes(MARKER),\n );\n if (match) return match;\n if (comments.length < 100) return null;\n }\n}\n\nasync function upsertComment(input: {\n token: string;\n owner: string;\n repo: string;\n issue: string;\n body: string;\n /** When true, refresh an existing comment but never create a new one. */\n updateOnly?: boolean;\n}): Promise<{\n action: \"created\" | \"updated\" | \"skipped\";\n id: number;\n html_url?: string;\n}> {\n const body = input.body.includes(MARKER)\n ? input.body\n : `${MARKER}\\n${input.body}`;\n const existing = await findExistingComment(input);\n if (!existing && input.updateOnly) {\n // Nothing to refresh and we were told not to create — e.g. a tiny diff with\n // no prior recap. Stay silent rather than posting a \"skipped\" comment.\n return { action: \"skipped\", id: 0 };\n }\n if (existing) {\n const updated = await githubRequest<GitHubComment>(\n input.token,\n `/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(\n input.repo,\n )}/issues/comments/${existing.id}`,\n {\n method: \"PATCH\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ body }),\n },\n );\n return { action: \"updated\", id: existing.id, html_url: updated.html_url };\n }\n const created = await githubRequest<GitHubComment>(\n input.token,\n `/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(\n input.repo,\n )}/issues/${encodeURIComponent(input.issue)}/comments`,\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ body }),\n },\n );\n return { action: \"created\", id: created.id, html_url: created.html_url };\n}\n\nfunction planIdFromUrl(url: string): string | null {\n const match = url.match(/\\/plans\\/([A-Za-z0-9_-]+)/);\n return match ? match[1] : null;\n}\n\n/** True when both URLs parse and share an origin. */\nfunction sameOrigin(a: string, b: string): boolean {\n try {\n return new URL(a).origin === new URL(b).origin;\n } catch {\n return false;\n }\n}\n\n/** The origin of a URL, or \"\" if it doesn't parse. */\nfunction originOf(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"\";\n }\n}\n\n/** Build the sticky comment body from the workflow's environment. */\nexport function buildCommentBody(env: NodeJS.ProcessEnv = process.env): string {\n const headShort = (env.HEAD_SHA || \"\").slice(0, 7);\n const aid =\n \"_A visual recap is an aid, not a replacement for reviewing the diff._\";\n const lines: string[] = [MARKER];\n\n if (env.SUPPRESSED === \"true\") {\n let reason = \"potential secret in diff\";\n try {\n const parsed = JSON.parse(env.SUPPRESSED_JSON || \"{}\");\n if (parsed && typeof parsed.reason === \"string\") reason = parsed.reason;\n } catch {\n /* keep default */\n }\n lines.push(\"### Visual recap — not generated\");\n lines.push(\"\");\n lines.push(\n \"The recap was **suppressed** because the diff matched a secret/credential pattern. No plan was published.\",\n );\n lines.push(\"\");\n lines.push(`Reason: \\`${reason}\\`. Updated for \\`${headShort}\\`.`);\n lines.push(\"\");\n lines.push(aid);\n return lines.join(\"\\n\");\n }\n\n // Tiny diffs aren't worth a recap. Refresh an existing sticky comment to this\n // state (the workflow only updates, never creates, on tiny) so it never lingers\n // pointing at a stale head SHA.\n if (env.DIFF_TINY === \"true\") {\n lines.push(\"### Visual recap — skipped (diff too small)\");\n lines.push(\"\");\n lines.push(\n \"The change in this push is too small to be worth a visual recap. This is informational only and does **not** block the PR.\",\n );\n lines.push(\"\");\n lines.push(`Updated for \\`${headShort}\\`.`);\n lines.push(\"\");\n lines.push(aid);\n return lines.join(\"\\n\");\n }\n\n const planUrl = (env.PLAN_URL || \"\").trim();\n const appUrl = (env.PLAN_RECAP_APP_URL || \"\").trim();\n // recap-url.txt is agent-written → untrusted. Rebuild a canonical link from a\n // TRUSTED base (the configured PLAN_RECAP_APP_URL when set, else the parsed\n // origin of the plan URL) plus a strictly-validated plan id, instead of\n // embedding the raw URL. That both enforces the app origin and prevents\n // markdown injection — a same-origin URL with a crafted path/query could\n // otherwise break out of the markdown link.\n const planId = planUrl ? planIdFromUrl(planUrl) : null;\n const sameOriginOk = appUrl === \"\" || sameOrigin(planUrl, appUrl);\n const base = (appUrl || originOf(planUrl)).replace(/\\/$/, \"\");\n const safeUrl =\n planId && base && sameOriginOk ? `${base}/plans/${planId}` : \"\";\n if (!safeUrl) {\n lines.push(\"### Visual recap — generation failed\");\n lines.push(\"\");\n lines.push(\n \"The visual recap could not be generated for this push. This is informational only and does **not** block the PR.\",\n );\n lines.push(\"\");\n lines.push(`Updated for \\`${headShort}\\`.`);\n lines.push(\"\");\n lines.push(aid);\n return lines.join(\"\\n\");\n }\n\n // The image URL is produced by our own recap-image route, but validate it is\n // same-origin and matches the canonical hex-token path before embedding it, so\n // it likewise cannot inject markdown.\n const imageUrlRaw = (env.RECAP_IMAGE_URL || \"\").trim();\n const imageUrl =\n imageUrlRaw &&\n sameOrigin(imageUrlRaw, base) &&\n /\\/_agent-native\\/recap-image\\/[0-9a-f]+\\.png$/.test(imageUrlRaw)\n ? imageUrlRaw\n : \"\";\n lines.push(\"### Visual recap — review at a higher altitude\");\n lines.push(\"\");\n if (imageUrl) {\n lines.push(`[](${safeUrl})`);\n lines.push(\"\");\n }\n lines.push(`**[Open the interactive recap](${safeUrl})**`);\n if (env.DIFF_HUGE === \"true\") {\n lines.push(\"\");\n lines.push(\n \"> Large diff — this recap is a **summarized** view (top files + schema/API deltas).\",\n );\n }\n lines.push(\"\");\n lines.push(`Updated for \\`${headShort}\\`. ${aid}`);\n lines.push(\"\");\n lines.push(`<!-- plan-id: ${planId} -->`);\n return lines.join(\"\\n\");\n}\n\n/* -------------------------------------------------------------------------- */\n/* Subcommands */\n/* -------------------------------------------------------------------------- */\n\nfunction runScan(args: Record<string, string | boolean>): void {\n const diffPath = stringArg(args, \"diff\");\n const diffText = fs.readFileSync(path.resolve(diffPath), \"utf8\");\n if (diffContainsSecret(diffText)) {\n process.stdout.write(\n `${JSON.stringify({ suppressed: true, reason: \"potential secret in diff\" })}\\n`,\n );\n } else {\n process.stdout.write(`${JSON.stringify({ suppressed: false })}\\n`);\n }\n}\n\nfunction runBuildPrompt(args: Record<string, string | boolean>): void {\n const skill = readRepoSkillMd();\n const prompt = buildRecapPrompt({\n skillMd: skill.text,\n pr: stringArg(args, \"pr\"),\n head: optionalArg(args, \"head\"),\n appUrl: optionalArg(args, \"app-url\") ?? \"https://plan.agent-native.com\",\n diffPath: optionalArg(args, \"diff\") ?? \"recap.diff\",\n statPath: optionalArg(args, \"stat\"),\n prevPlanId: optionalArg(args, \"prev-plan-id\"),\n huge: args.huge === true || args.huge === \"true\",\n });\n const out = optionalArg(args, \"out\") ?? \"recap-prompt.md\";\n fs.writeFileSync(path.resolve(out), prompt);\n process.stdout.write(\n `${JSON.stringify({ ok: true, out, skillSource: skill.source, bytes: prompt.length })}\\n`,\n );\n}\n\n/** Upload a PNG to the plan app's signed public image route; returns its URL. */\nasync function uploadRecapImage(input: {\n appUrl: string;\n token: string;\n pngPath: string;\n}): Promise<string | null> {\n try {\n const base = input.appUrl.replace(/\\/$/, \"\");\n const bytes = fs.readFileSync(path.resolve(input.pngPath));\n const res = await fetch(`${base}/_agent-native/recap-image`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"image/png\",\n authorization: `Bearer ${input.token}`,\n },\n body: bytes,\n });\n // Surface failures on stderr — stdout carries the machine-readable JSON the\n // workflow parses, so it must stay clean. A silent null here is exactly what\n // made the missing-inline-thumbnail failure undebuggable from CI logs.\n if (!res.ok) {\n const detail = await res.text().catch(() => \"\");\n process.stderr.write(\n `[recap shot] image upload failed: ${res.status} ${res.statusText} ${detail.slice(0, 300)}\\n`,\n );\n return null;\n }\n const json = (await res.json().catch(() => null)) as {\n imageUrl?: string;\n } | null;\n if (!json?.imageUrl) {\n process.stderr.write(\n `[recap shot] image upload returned no imageUrl (status ${res.status})\\n`,\n );\n return null;\n }\n return json.imageUrl;\n } catch (err) {\n process.stderr.write(`[recap shot] image upload error: ${String(err)}\\n`);\n return null;\n }\n}\n\nasync function runShot(args: Record<string, string | boolean>): Promise<void> {\n const url = stringArg(args, \"url\");\n const out = optionalArg(args, \"out\") ?? \"recap.png\";\n const token = optionalArg(args, \"token\");\n const appUrl = optionalArg(args, \"app-url\");\n\n const done = (obj: Record<string, unknown>) => {\n process.stdout.write(`${JSON.stringify(obj)}\\n`);\n };\n\n // recap-url.txt is produced by the (LLM) agent, so the URL is untrusted. Only\n // forward the reusable publish token to the trusted plan-app origin — never to\n // an arbitrary URL — so a poisoned recap-url.txt can't exfiltrate the bearer\n // to an attacker-controlled domain.\n let attachToken = false;\n if (token) {\n try {\n attachToken = !!appUrl && new URL(url).origin === new URL(appUrl).origin;\n } catch {\n attachToken = false;\n }\n if (!attachToken) {\n done({\n ok: false,\n reason: appUrl\n ? `refusing to screenshot ${url}: origin does not match --app-url (${appUrl}); the publish token is only sent to the trusted plan app origin`\n : `refusing to attach the publish token without --app-url to validate ${url} against`,\n });\n return;\n }\n }\n\n let chromium: typeof import(\"playwright\").chromium | undefined;\n try {\n ({ chromium } = await import(\"playwright\"));\n } catch {\n try {\n ({ chromium } =\n (await import(\"@playwright/test\")) as unknown as typeof import(\"playwright\"));\n } catch (err) {\n done({ ok: false, reason: `playwright not available: ${String(err)}` });\n return;\n }\n }\n\n let captured = false;\n let browser: import(\"playwright\").Browser | undefined;\n const hardTimer = setTimeout(() => {\n done({ ok: false, reason: \"hard 60s timeout reached\" });\n process.exit(0);\n }, 60_000);\n try {\n browser = await chromium!.launch({ args: [\"--no-sandbox\"] });\n const context = await browser.newContext({\n viewport: { width: 1280, height: 900 },\n deviceScaleFactor: 2,\n });\n if (attachToken) {\n // Attach the bearer ONLY to same-origin requests. Context-wide\n // extraHTTPHeaders would also send it to every cross-origin subresource\n // the plan page loads (CDN images/fonts/scripts), leaking the publish\n // token; routing scopes it to the trusted app origin.\n const appOrigin = new URL(appUrl as string).origin;\n await context.route(\"**/*\", async (route) => {\n const request = route.request();\n if (new URL(request.url()).origin === appOrigin) {\n await route.continue({\n headers: { ...request.headers(), authorization: `Bearer ${token}` },\n });\n } else {\n await route.continue();\n }\n });\n }\n const page = await context.newPage();\n await page.goto(url, { waitUntil: \"networkidle\", timeout: 45_000 });\n const selectors = [\n \"[data-plan-document]\",\n \"[data-plan-block]\",\n \"main article\",\n \"[data-testid='plan-document']\",\n \"main\",\n ];\n let matched = false;\n for (const sel of selectors) {\n try {\n await page.waitForSelector(sel, { timeout: 6_000, state: \"visible\" });\n matched = true;\n break;\n } catch {\n /* try the next selector */\n }\n }\n await page.waitForTimeout(matched ? 1_200 : 500);\n await page.screenshot({ path: out, fullPage: true });\n captured = true;\n await browser.close();\n } catch (err) {\n clearTimeout(hardTimer);\n try {\n if (browser) await browser.close();\n } catch {\n /* ignore */\n }\n done({\n ok: false,\n reason: err instanceof Error ? err.message : String(err),\n });\n return;\n }\n clearTimeout(hardTimer);\n\n let imageUrl: string | null = null;\n if (captured && token && appUrl) {\n imageUrl = await uploadRecapImage({ appUrl, token, pngPath: out });\n }\n done({ ok: captured, out, imageUrl });\n}\n\nasync function runComment(\n args: Record<string, string | boolean>,\n sub: string,\n): Promise<void> {\n const token = stringArg(args, \"token\");\n const { owner, repo } = repoParts(stringArg(args, \"repo\"));\n const issue = stringArg(args, \"issue\");\n\n if (sub === \"find-plan-id\") {\n const existing = await findExistingComment({ token, owner, repo, issue });\n const body = existing?.body ?? \"\";\n const match = body.match(/<!--\\s*plan-id:\\s*([^\\s]+)\\s*-->/);\n process.stdout.write(match ? match[1] : \"\");\n return;\n }\n\n if (sub === \"upsert\") {\n const result = await upsertComment({\n token,\n owner,\n repo,\n issue,\n body: buildCommentBody(),\n updateOnly:\n args[\"update-only\"] === true || args[\"update-only\"] === \"true\",\n });\n process.stdout.write(`${JSON.stringify(result)}\\n`);\n return;\n }\n\n throw new Error(\n \"Usage: agent-native recap comment <find-plan-id|upsert> --repo owner/name --issue n --token token\",\n );\n}\n\nconst HELP = `agent-native recap — PR visual recap helpers (used by the GitHub Action)\n\nUsage:\n agent-native recap scan --diff <path>\n agent-native recap build-prompt --pr <n> [--head <sha>] [--app-url <url>] [--diff <path>] [--stat <path>] [--prev-plan-id <id>] [--huge] [--out <path>]\n agent-native recap shot --url <planUrl> [--token <planToken>] [--app-url <url>] [--out recap.png]\n agent-native recap comment <find-plan-id|upsert> --repo owner/name --issue <n> --token <github-token>\n`;\n\nexport async function runRecap(argv: string[]): Promise<void> {\n const [sub, ...rest] = argv;\n const args = parseArgs(rest);\n switch (sub) {\n case \"scan\":\n runScan(args);\n return;\n case \"build-prompt\":\n runBuildPrompt(args);\n return;\n case \"shot\":\n await runShot(args);\n return;\n case \"comment\":\n await runComment(parseArgs(rest.slice(1)), rest[0] ?? \"\");\n return;\n case \"help\":\n case \"--help\":\n case \"-h\":\n case undefined:\n process.stdout.write(HELP);\n return;\n default:\n process.stderr.write(`Unknown recap subcommand: ${sub}\\n${HELP}`);\n process.exit(1);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"recap.js","sourceRoot":"","sources":["../../src/cli/recap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAE7E,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF,SAAS,SAAS,CAAC,IAAc;IAC/B,MAAM,GAAG,GAAqC,EAAE,CAAC;IACjD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACtB,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,SAAS;QACtC,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACzB,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;aAC5D,CAAC;YACJ,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;YAChB,CAAC,IAAI,CAAC,CAAC;QACT,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,SAAS,CAChB,IAAsC,EACtC,GAAW;IAEX,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACxB,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,aAAa,GAAG,EAAE,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,WAAW,CAClB,IAAsC,EACtC,GAAW;IAEX,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACxB,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;AAC3E,CAAC;AAED,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF,mEAAmE;AACnE,MAAM,CAAC,MAAM,qBAAqB,GAAa;IAC7C,mBAAmB;IACnB,iEAAiE;IACjE,wEAAwE;IACxE,yCAAyC;IACzC,+FAA+F;IAC/F,+JAA+J;IAC/J,iHAAiH;CAClH,CAAC;AAEF,+DAA+D;AAC/D,MAAM,UAAU,0BAA0B,CAAC,OAAe;IAIxD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;IAC1D,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,qBAAqB,CAAC,CAAC;IACnD,MAAM,OAAO,GAAG,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACpC,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,4BAA4B,CAAC,CAAC;IACrD,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC;AACzD,CAAC;AAED,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF;;;;;GAKG;AACH,MAAM,eAAe,GAAa;IAChC,gCAAgC;IAChC,mCAAmC;IACnC,0BAA0B;IAC1B,kCAAkC;IAClC,kCAAkC;IAClC,sBAAsB;IACtB,4BAA4B;IAC5B,6DAA6D;IAC7D,2DAA2D;IAC3D,sBAAsB;IACtB,6DAA6D;IAC7D,+EAA+E;IAC/E,iCAAiC;IACjC,gNAAgN;CACjN,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,OAAO,eAAe,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;AACrD,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,KAAK,MAAM,IAAI,IAAI,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACxC,IACE,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YACpB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YACpB,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;YACpB,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC;YACtB,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EACtB,CAAC;YACD,IAAI,eAAe,CAAC,IAAI,CAAC;gBAAE,OAAO,IAAI,CAAC;QACzC,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IAIzD,MAAM,UAAU,GAAG;QACjB,sCAAsC;QACtC,sCAAsC;QACtC,8BAA8B;QAC9B,qDAAqD;KACtD,CAAC;IACF,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACnC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,EAAE,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;QAC7D,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CACb,wFAAwF,CACzF,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAShC;IACC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,qDAAqD,CAAC,CAAC;IAClE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CACR,+KAA+K,CAChL,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,qDAAqD,CAAC,CAAC;IAClE,KAAK,CAAC,IAAI,CAAC,mBAAmB,KAAK,CAAC,EAAE,IAAI,CAAC,CAAC;IAC5C,IAAI,KAAK,CAAC,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,oBAAoB,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC;IAC/D,KAAK,CAAC,IAAI,CAAC,qBAAqB,KAAK,CAAC,QAAQ,qBAAqB,CAAC,CAAC;IACrE,IAAI,KAAK,CAAC,QAAQ;QAChB,KAAK,CAAC,IAAI,CAAC,kBAAkB,KAAK,CAAC,QAAQ,qBAAqB,CAAC,CAAC;IACpE,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;QACf,KAAK,CAAC,IAAI,CACR,8GAA8G,CAC/G,CAAC;IACJ,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,qDAAqD,CAAC,CAAC;IAClE,KAAK,CAAC,IAAI,CACR,iLAAiL,CAClL,CAAC;IACF,KAAK,CAAC,IAAI,CACR,wHACE,KAAK,CAAC,UAAU;QACd,CAAC,CAAC,wBAAwB,KAAK,CAAC,UAAU,8CAA8C;QACxF,CAAC,CAAC,EACN,GAAG,CACJ,CAAC;IACF,KAAK,CAAC,IAAI,CACR,wNAAwN,CACzN,CAAC;IACF,KAAK,CAAC,IAAI,CACR,4GAA4G,MAAM,8HAA8H,CACjP,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CACR,kMAAkM,CACnM,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,4CAA4C,CAAC,CAAC;IACzD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IACjC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF,MAAM,MAAM,GAAG,0BAA0B,CAAC;AAS1C,SAAS,SAAS,CAAC,YAAoB;IACrC,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9C,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI;QAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,YAAY,EAAE,CAAC,CAAC;IACxE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACzB,CAAC;AAED,KAAK,UAAU,aAAa,CAC1B,KAAa,EACb,OAAe,EACf,OAAoB,EAAE;IAEtB,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,yBAAyB,OAAO,EAAE,EAAE;QAC1D,GAAG,IAAI;QACP,OAAO,EAAE;YACP,MAAM,EAAE,6BAA6B;YACrC,aAAa,EAAE,UAAU,KAAK,EAAE;YAChC,sBAAsB,EAAE,YAAY;YACpC,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;SACxB;KACF,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAChD,MAAM,IAAI,KAAK,CACb,yBAAyB,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CACjF,CAAC;IACJ,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG;QAAE,OAAO,SAAc,CAAC;IAC9C,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAM,CAAC;AACjC,CAAC;AAED,KAAK,UAAU,mBAAmB,CAAC,KAKlC;IACC,KAAK,IAAI,IAAI,GAAG,CAAC,GAAI,IAAI,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,MAAM,aAAa,CAClC,KAAK,CAAC,KAAK,EACX,UAAU,kBAAkB,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,kBAAkB,CAC7D,KAAK,CAAC,IAAI,CACX,WAAW,kBAAkB,CAAC,KAAK,CAAC,KAAK,CAAC,+BAA+B,IAAI,EAAE,CACjF,CAAC;QACF,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CACzB,CAAC,OAAO,EAAE,EAAE,CACV,OAAO,CAAC,IAAI,EAAE,IAAI,KAAK,KAAK;YAC5B,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ;YAChC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAChC,CAAC;QACF,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC;QACxB,IAAI,QAAQ,CAAC,MAAM,GAAG,GAAG;YAAE,OAAO,IAAI,CAAC;IACzC,CAAC;AACH,CAAC;AAED,KAAK,UAAU,aAAa,CAAC,KAQ5B;IAKC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;QACtC,CAAC,CAAC,KAAK,CAAC,IAAI;QACZ,CAAC,CAAC,GAAG,MAAM,KAAK,KAAK,CAAC,IAAI,EAAE,CAAC;IAC/B,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,KAAK,CAAC,CAAC;IAClD,IAAI,CAAC,QAAQ,IAAI,KAAK,CAAC,UAAU,EAAE,CAAC;QAClC,4EAA4E;QAC5E,uEAAuE;QACvE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC;IACtC,CAAC;IACD,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,MAAM,aAAa,CACjC,KAAK,CAAC,KAAK,EACX,UAAU,kBAAkB,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,kBAAkB,CAC7D,KAAK,CAAC,IAAI,CACX,oBAAoB,QAAQ,CAAC,EAAE,EAAE,EAClC;YACE,MAAM,EAAE,OAAO;YACf,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;YAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC;SAC/B,CACF,CAAC;QACF,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;IAC5E,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,aAAa,CACjC,KAAK,CAAC,KAAK,EACX,UAAU,kBAAkB,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,kBAAkB,CAC7D,KAAK,CAAC,IAAI,CACX,WAAW,kBAAkB,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,EACtD;QACE,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,CAAC;KAC/B,CACF,CAAC;IACF,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,EAAE,OAAO,CAAC,EAAE,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC;AAC3E,CAAC;AAED,SAAS,aAAa,CAAC,GAAW;IAChC,4EAA4E;IAC5E,6EAA6E;IAC7E,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,sCAAsC,CAAC,CAAC;IAChE,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACjC,CAAC;AAED,qDAAqD;AACrD,SAAS,UAAU,CAAC,CAAS,EAAE,CAAS;IACtC,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,sDAAsD;AACtD,SAAS,QAAQ,CAAC,GAAW;IAC3B,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,gBAAgB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACnE,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACnD,MAAM,KAAK,GAAa,CAAC,MAAM,CAAC,CAAC;IAEjC,IAAI,GAAG,CAAC,UAAU,KAAK,MAAM,EAAE,CAAC;QAC9B,IAAI,MAAM,GAAG,0BAA0B,CAAC;QACxC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,eAAe,IAAI,IAAI,CAAC,CAAC;YACvD,IAAI,MAAM,IAAI,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ;gBAAE,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;QAC1E,CAAC;QAAC,MAAM,CAAC;YACP,kBAAkB;QACpB,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;QAC/C,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CACR,2GAA2G,CAC5G,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,aAAa,MAAM,qBAAqB,SAAS,KAAK,CAAC,CAAC;QACnE,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,8EAA8E;IAC9E,gFAAgF;IAChF,gCAAgC;IAChC,IAAI,GAAG,CAAC,SAAS,KAAK,MAAM,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC,6CAA6C,CAAC,CAAC;QAC1D,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CACR,4HAA4H,CAC7H,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,iBAAiB,SAAS,KAAK,CAAC,CAAC;QAC5C,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5C,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,kBAAkB,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACrD,8EAA8E;IAC9E,4EAA4E;IAC5E,wEAAwE;IACxE,wEAAwE;IACxE,yEAAyE;IACzE,4CAA4C;IAC5C,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACvD,MAAM,YAAY,GAAG,MAAM,KAAK,EAAE,IAAI,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAClE,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC9D,MAAM,OAAO,GACX,MAAM,IAAI,IAAI,IAAI,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI,WAAW,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACnE,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,KAAK,CAAC,IAAI,CAAC,sCAAsC,CAAC,CAAC;QACnD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CACR,kHAAkH,CACnH,CAAC;QACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CAAC,iBAAiB,SAAS,KAAK,CAAC,CAAC;QAC5C,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC;IAED,6EAA6E;IAC7E,+EAA+E;IAC/E,sCAAsC;IACtC,MAAM,WAAW,GAAG,CAAC,GAAG,CAAC,eAAe,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACvD,MAAM,QAAQ,GACZ,WAAW;QACX,UAAU,CAAC,WAAW,EAAE,IAAI,CAAC;QAC7B,+CAA+C,CAAC,IAAI,CAAC,WAAW,CAAC;QAC/D,CAAC,CAAC,WAAW;QACb,CAAC,CAAC,EAAE,CAAC;IACT,KAAK,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;IAC7D,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,IAAI,QAAQ,EAAE,CAAC;QACb,KAAK,CAAC,IAAI,CAAC,oBAAoB,QAAQ,MAAM,OAAO,GAAG,CAAC,CAAC;QACzD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,kCAAkC,OAAO,KAAK,CAAC,CAAC;IAC3D,IAAI,GAAG,CAAC,SAAS,KAAK,MAAM,EAAE,CAAC;QAC7B,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACf,KAAK,CAAC,IAAI,CACR,qFAAqF,CACtF,CAAC;IACJ,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,iBAAiB,SAAS,KAAK,CAAC,CAAC;IAC5C,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,iBAAiB,MAAM,MAAM,CAAC,CAAC;IAC1C,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC;AAED,gFAAgF;AAChF,gFAAgF;AAChF,gFAAgF;AAEhF,SAAS,OAAO,CAAC,IAAsC;IACrD,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACzC,MAAM,QAAQ,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;IACjE,IAAI,kBAAkB,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjC,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,0BAA0B,EAAE,CAAC,IAAI,CAChF,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,CAAC;IACrE,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,IAAsC;IAC5D,MAAM,KAAK,GAAG,eAAe,EAAE,CAAC;IAChC,MAAM,MAAM,GAAG,gBAAgB,CAAC;QAC9B,OAAO,EAAE,KAAK,CAAC,IAAI;QACnB,EAAE,EAAE,SAAS,CAAC,IAAI,EAAE,IAAI,CAAC;QACzB,IAAI,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC;QAC/B,MAAM,EAAE,WAAW,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,+BAA+B;QACvE,QAAQ,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,YAAY;QACnD,QAAQ,EAAE,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC;QACnC,UAAU,EAAE,WAAW,CAAC,IAAI,EAAE,cAAc,CAAC;QAC7C,IAAI,EAAE,IAAI,CAAC,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;KACjD,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,iBAAiB,CAAC;IAC1D,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;IAC5C,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,EAAE,WAAW,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,CAC1F,CAAC;AACJ,CAAC;AAED,iFAAiF;AACjF,KAAK,UAAU,gBAAgB,CAAC,KAI/B;IACC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC7C,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAC3D,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,4BAA4B,EAAE;YAC3D,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,WAAW;gBAC3B,aAAa,EAAE,UAAU,KAAK,CAAC,KAAK,EAAE;aACvC;YACD,IAAI,EAAE,KAAK;SACZ,CAAC,CAAC;QACH,4EAA4E;QAC5E,6EAA6E;QAC7E,uEAAuE;QACvE,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAChD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,qCAAqC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,UAAU,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,CAC9F,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAExC,CAAC;QACT,IAAI,CAAC,IAAI,EAAE,QAAQ,EAAE,CAAC;YACpB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,0DAA0D,GAAG,CAAC,MAAM,KAAK,CAC1E,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,oCAAoC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAC1E,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,OAAO,CAAC,IAAsC;IAC3D,MAAM,GAAG,GAAG,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,WAAW,CAAC;IACpD,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,WAAW,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAE5C,MAAM,IAAI,GAAG,CAAC,GAA4B,EAAE,EAAE;QAC5C,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACnD,CAAC,CAAC;IAEF,8EAA8E;IAC9E,+EAA+E;IAC/E,6EAA6E;IAC7E,oCAAoC;IACpC,IAAI,WAAW,GAAG,KAAK,CAAC;IACxB,IAAI,KAAK,EAAE,CAAC;QACV,IAAI,CAAC;YACH,WAAW,GAAG,CAAC,CAAC,MAAM,IAAI,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC;QAC3E,CAAC;QAAC,MAAM,CAAC;YACP,WAAW,GAAG,KAAK,CAAC;QACtB,CAAC;QACD,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,IAAI,CAAC;gBACH,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,MAAM;oBACZ,CAAC,CAAC,0BAA0B,GAAG,sCAAsC,MAAM,kEAAkE;oBAC7I,CAAC,CAAC,sEAAsE,GAAG,UAAU;aACxF,CAAC,CAAC;YACH,OAAO;QACT,CAAC;IACH,CAAC;IAED,IAAI,QAA0D,CAAC;IAC/D,IAAI,CAAC;QACH,CAAC,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC;IAC9C,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,CAAC;YACH,CAAC,EAAE,QAAQ,EAAE;gBACX,CAAC,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAA2C,CAAC,CAAC;QAClF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,6BAA6B,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;YACxE,OAAO;QACT,CAAC;IACH,CAAC;IAED,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,IAAI,OAAiD,CAAC;IACtD,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;QAChC,IAAI,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,0BAA0B,EAAE,CAAC,CAAC;QACxD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,EAAE,MAAM,CAAC,CAAC;IACX,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,QAAS,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC;QAC7D,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC;YACvC,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;YACvC,iBAAiB,EAAE,CAAC;SACrB,CAAC,CAAC;QACH,IAAI,WAAW,EAAE,CAAC;YAChB,+DAA+D;YAC/D,wEAAwE;YACxE,sEAAsE;YACtE,sDAAsD;YACtD,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,MAAgB,CAAC,CAAC,MAAM,CAAC;YACnD,MAAM,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE;gBAC1C,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,EAAE,CAAC;gBAChC,IAAI,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;oBAChD,MAAM,KAAK,CAAC,QAAQ,CAAC;wBACnB,OAAO,EAAE,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE;qBACpE,CAAC,CAAC;gBACL,CAAC;qBAAM,CAAC;oBACN,MAAM,KAAK,CAAC,QAAQ,EAAE,CAAC;gBACzB,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC;QACD,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACrC,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;QACpE,MAAM,SAAS,GAAG;YAChB,sBAAsB;YACtB,mBAAmB;YACnB,cAAc;YACd,+BAA+B;YAC/B,MAAM;SACP,CAAC;QACF,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;YAC5B,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,eAAe,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;gBACtE,OAAO,GAAG,IAAI,CAAC;gBACf,MAAM;YACR,CAAC;YAAC,MAAM,CAAC;gBACP,2BAA2B;YAC7B,CAAC;QACH,CAAC;QACD,MAAM,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QACjD,6EAA6E;QAC7E,wEAAwE;QACxE,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;YACtB,QAAQ,CAAC,eAA+B,CAAC,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC;QAC/D,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;QACrC,QAAQ,GAAG,IAAI,CAAC;QAChB,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,YAAY,CAAC,SAAS,CAAC,CAAC;QACxB,IAAI,CAAC;YACH,IAAI,OAAO;gBAAE,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QACrC,CAAC;QAAC,MAAM,CAAC;YACP,YAAY;QACd,CAAC;QACD,IAAI,CAAC;YACH,EAAE,EAAE,KAAK;YACT,MAAM,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACzD,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IACD,YAAY,CAAC,SAAS,CAAC,CAAC;IAExB,IAAI,QAAQ,GAAkB,IAAI,CAAC;IACnC,IAAI,QAAQ,IAAI,KAAK,IAAI,MAAM,EAAE,CAAC;QAChC,QAAQ,GAAG,MAAM,gBAAgB,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;IACrE,CAAC;IACD,IAAI,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,QAAQ,EAAE,CAAC,CAAC;AACxC,CAAC;AAED,KAAK,UAAU,UAAU,CACvB,IAAsC,EACtC,GAAW;IAEX,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACvC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3D,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAEvC,IAAI,GAAG,KAAK,cAAc,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,MAAM,mBAAmB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QAC1E,MAAM,IAAI,GAAG,QAAQ,EAAE,IAAI,IAAI,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;QAC7D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5C,OAAO;IACT,CAAC;IAED,IAAI,GAAG,KAAK,QAAQ,EAAE,CAAC;QACrB,MAAM,MAAM,GAAG,MAAM,aAAa,CAAC;YACjC,KAAK;YACL,KAAK;YACL,IAAI;YACJ,KAAK;YACL,IAAI,EAAE,gBAAgB,EAAE;YACxB,UAAU,EACR,IAAI,CAAC,aAAa,CAAC,KAAK,IAAI,IAAI,IAAI,CAAC,aAAa,CAAC,KAAK,MAAM;SACjE,CAAC,CAAC;QACH,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QACpD,OAAO;IACT,CAAC;IAED,MAAM,IAAI,KAAK,CACb,mGAAmG,CACpG,CAAC;AACJ,CAAC;AAED,MAAM,IAAI,GAAG;;;;;;;CAOZ,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,IAAc;IAC3C,MAAM,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAC5B,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IAC7B,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,MAAM;YACT,OAAO,CAAC,IAAI,CAAC,CAAC;YACd,OAAO;QACT,KAAK,cAAc;YACjB,cAAc,CAAC,IAAI,CAAC,CAAC;YACrB,OAAO;QACT,KAAK,MAAM;YACT,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;YACpB,OAAO;QACT,KAAK,SAAS;YACZ,MAAM,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YAC1D,OAAO;QACT,KAAK,MAAM,CAAC;QACZ,KAAK,QAAQ,CAAC;QACd,KAAK,IAAI,CAAC;QACV,KAAK,SAAS;YACZ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC3B,OAAO;QACT;YACE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC;YAClE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;AACH,CAAC","sourcesContent":["/**\n * `agent-native recap <scan|build-prompt|shot|comment>` — the helper surface\n * used by the PR Visual Recap GitHub Action.\n *\n * The action no longer generates the recap deterministically. Instead a coding\n * agent (Claude Code or Codex) RUNS THE REPO'S visual-recap skill against the\n * diff and publishes the plan via the plan MCP tools. These subcommands are the\n * thin, deterministic glue around that:\n *\n * scan Refuse to hand a secret-leaking diff to the agent.\n * build-prompt Assemble the agent prompt = repo SKILL.md + a task wrapper.\n * shot Screenshot the published plan and upload it to the plan app's\n * signed public image route (for an inline PR-comment image).\n * comment Find the previous plan id / upsert the sticky PR comment.\n *\n * Promoting these to the published CLI means an installed repo's workflow calls\n * `agent-native recap …` instead of copying helper scripts into the repo.\n *\n * Node built-ins only (plus an optional dynamic `playwright` import for `shot`).\n */\n\nimport fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { PR_VISUAL_RECAP_WORKFLOW_YML } from \"./pr-visual-recap-workflow.js\";\n\n/* -------------------------------------------------------------------------- */\n/* Arg parsing */\n/* -------------------------------------------------------------------------- */\n\nfunction parseArgs(argv: string[]): Record<string, string | boolean> {\n const out: Record<string, string | boolean> = {};\n for (let i = 0; i < argv.length; i += 1) {\n const token = argv[i];\n if (!token.startsWith(\"--\")) continue;\n const key = token.slice(2);\n const next = argv[i + 1];\n if (next === undefined || next.startsWith(\"--\")) out[key] = true;\n else {\n out[key] = next;\n i += 1;\n }\n }\n return out;\n}\n\nfunction stringArg(\n args: Record<string, string | boolean>,\n key: string,\n): string {\n const value = args[key];\n if (typeof value !== \"string\" || value.length === 0) {\n throw new Error(`Missing --${key}`);\n }\n return value;\n}\n\nfunction optionalArg(\n args: Record<string, string | boolean>,\n key: string,\n): string | undefined {\n const value = args[key];\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\n/* -------------------------------------------------------------------------- */\n/* GitHub Action install (used by `skills add … --with-github-action`) */\n/* -------------------------------------------------------------------------- */\n\n/** GitHub secrets the installed PR Visual Recap workflow needs. */\nexport const PR_VISUAL_RECAP_SETUP: string[] = [\n \"Required secrets:\",\n \" PLAN_RECAP_TOKEN — bearer token from `agent-native connect`\",\n \" ANTHROPIC_API_KEY — the LLM key for the default Claude Code backend\",\n \"Optional (only if you change defaults):\",\n \" OPENAI_API_KEY (secret) + VISUAL_RECAP_AGENT=codex (variable) — use Codex instead of Claude\",\n \" VISUAL_RECAP_MODEL / VISUAL_RECAP_REASONING (variables) — pin the model (e.g. gpt-5.5) and reasoning depth (none|minimal|low|medium|high|xhigh; Codex only)\",\n \" PLAN_RECAP_APP_URL (secret) — only when self-hosting the plan app (defaults to https://plan.agent-native.com)\",\n];\n\n/** Write .github/workflows/pr-visual-recap.yml into a repo. */\nexport function writePrVisualRecapWorkflow(baseDir: string): {\n path: string;\n existed: boolean;\n} {\n const dir = path.resolve(baseDir, \".github\", \"workflows\");\n fs.mkdirSync(dir, { recursive: true });\n const file = path.join(dir, \"pr-visual-recap.yml\");\n const existed = fs.existsSync(file);\n fs.writeFileSync(file, PR_VISUAL_RECAP_WORKFLOW_YML);\n return { path: path.relative(baseDir, file), existed };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Secret scan — defense-in-depth before any LLM sees the diff */\n/* -------------------------------------------------------------------------- */\n\n/**\n * If the diff contains anything that looks like a real secret, we refuse to\n * build a recap at all (rather than risk echoing it into a published plan).\n * These patterns intentionally err toward caution and scan added, removed, and\n * context lines so deleting a real secret does not leak it in a split diff.\n */\nconst SECRET_PATTERNS: RegExp[] = [\n // Common provider key prefixes.\n /\\b(?:sk|pk|rk)-[A-Za-z0-9]{16,}\\b/,\n /\\bghp_[A-Za-z0-9]{20,}\\b/,\n /\\bgithub_pat_[A-Za-z0-9_]{20,}\\b/,\n /\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b/,\n /\\bAKIA[0-9A-Z]{16}\\b/,\n /\\bAIza[0-9A-Za-z_-]{20,}\\b/,\n // Bearer / Authorization header values with an actual token.\n /authorization\\s*[:=]\\s*['\"]?bearer\\s+[A-Za-z0-9._-]{12,}/i,\n // Private key blocks.\n /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/,\n // `KEY=...`, `TOKEN=...`, `SECRET=...`, `PASSWORD=...` assigned a real-looking\n // value (long, non-placeholder).\n /\\b[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|API_KEY|PRIVATE_KEY|ACCESS_KEY)[A-Z0-9_]*\\s*[:=]\\s*['\"]?(?!.*(?:your|example|placeholder|changeme|xxxx|\\*\\*\\*|<|\\$\\{|process\\.env|env\\.|REDACTED))[A-Za-z0-9/_+=.-]{16,}/i,\n];\n\nexport function lineLooksSecret(line: string): boolean {\n return SECRET_PATTERNS.some((re) => re.test(line));\n}\n\nexport function diffContainsSecret(diffText: string): boolean {\n for (const line of diffText.split(\"\\n\")) {\n if (\n line.startsWith(\"+\") ||\n line.startsWith(\"-\") ||\n line.startsWith(\" \") ||\n line.startsWith(\"+++\") ||\n line.startsWith(\"---\")\n ) {\n if (lineLooksSecret(line)) return true;\n }\n }\n return false;\n}\n\n/* -------------------------------------------------------------------------- */\n/* Prompt builder — repo SKILL.md + task wrapper */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Locate the repo's visual-recap SKILL.md, preferring the host-agent install\n * locations so a user's `agent-native skills add` copy wins, then falling back\n * to the framework's own source locations.\n */\nexport function readRepoSkillMd(cwd: string = process.cwd()): {\n text: string;\n source: string;\n} {\n const candidates = [\n \".claude/skills/visual-recap/SKILL.md\",\n \".agents/skills/visual-recap/SKILL.md\",\n \"skills/visual-recap/SKILL.md\",\n \"templates/plan/.agents/skills/visual-recap/SKILL.md\",\n ];\n for (const rel of candidates) {\n const abs = path.resolve(cwd, rel);\n if (fs.existsSync(abs)) {\n return { text: fs.readFileSync(abs, \"utf8\"), source: rel };\n }\n }\n throw new Error(\n \"Could not find visual-recap/SKILL.md. Run `agent-native skills add visual-plan` first.\",\n );\n}\n\nexport function buildRecapPrompt(input: {\n skillMd: string;\n pr: string;\n head?: string;\n appUrl: string;\n diffPath: string;\n statPath?: string;\n prevPlanId?: string;\n huge?: boolean;\n}): string {\n const appUrl = input.appUrl.replace(/\\/$/, \"\");\n const lines: string[] = [];\n lines.push(\"# Task: publish a Visual Recap of this pull request\");\n lines.push(\"\");\n lines.push(\n `You are running non-interactively in CI. Follow the **visual-recap skill** included verbatim below to turn this PR's diff into a grounded Agent-Native Plan, then publish it.`,\n );\n lines.push(\"\");\n lines.push(\"## Inputs (read them from disk with your Read tool)\");\n lines.push(`- PR number: **#${input.pr}**`);\n if (input.head) lines.push(`- Head commit: \\`${input.head}\\``);\n lines.push(`- Unified diff: \\`${input.diffPath}\\` (read this file)`);\n if (input.statPath)\n lines.push(`- Diff stat: \\`${input.statPath}\\` (read this file)`);\n if (input.huge) {\n lines.push(\n `- The diff is LARGE — produce a **summarized** recap (top files + schema/API deltas), not an exhaustive one.`,\n );\n }\n lines.push(\"\");\n lines.push(\"## Publish (this is the only way to produce output)\");\n lines.push(\n `The \\`plan\\` MCP server is configured for you. Call its tools by name (your host may expose them as \\`create-visual-recap\\` or \\`mcp__plan__create-visual-recap\\` — same tool).`,\n );\n lines.push(\n `1. Call the **create-visual-recap** tool on the \\`plan\\` MCP server with grounded MDX derived ONLY from the real diff${\n input.prevPlanId\n ? `, passing \\`planId: \"${input.prevPlanId}\"\\` so this REPLACES the existing recap plan`\n : \"\"\n }.`,\n );\n lines.push(\n `2. Call the **set-resource-visibility** tool on the \\`plan\\` MCP server with \\`{ resourceType: \"plan\", resourceId: <the returned plan id>, visibility: \"org\" }\\` so the recap is login-gated to the org, never public.`,\n );\n lines.push(\n `3. Write the plan URL to a file named \\`recap-url.txt\\` at the repo root, containing exactly one line: \\`${appUrl}/recaps/<the returned plan id>\\`. This file is the workflow's only hand-off — do not print anything else as the deliverable.`,\n );\n lines.push(\"\");\n lines.push(\n \"Do not invent file names, schema fields, or endpoints. Redact anything that looks like a secret. If the diff has no reviewable substance, still publish a minimal recap and write recap-url.txt.\",\n );\n lines.push(\"\");\n lines.push(\"---\");\n lines.push(\"\");\n lines.push(\"# visual-recap skill (follow this exactly)\");\n lines.push(\"\");\n lines.push(input.skillMd.trim());\n lines.push(\"\");\n return lines.join(\"\\n\");\n}\n\n/* -------------------------------------------------------------------------- */\n/* GitHub comment helpers */\n/* -------------------------------------------------------------------------- */\n\nconst MARKER = \"<!-- pr-visual-recap -->\";\n\ntype GitHubComment = {\n id: number;\n body?: string | null;\n html_url?: string;\n user?: { type?: string | null } | null;\n};\n\nfunction repoParts(repoFullName: string): { owner: string; repo: string } {\n const [owner, repo] = repoFullName.split(\"/\");\n if (!owner || !repo) throw new Error(`Invalid --repo: ${repoFullName}`);\n return { owner, repo };\n}\n\nasync function githubRequest<T>(\n token: string,\n apiPath: string,\n init: RequestInit = {},\n): Promise<T> {\n const res = await fetch(`https://api.github.com${apiPath}`, {\n ...init,\n headers: {\n accept: \"application/vnd.github+json\",\n authorization: `Bearer ${token}`,\n \"x-github-api-version\": \"2022-11-28\",\n ...(init.headers ?? {}),\n },\n });\n if (!res.ok) {\n const detail = await res.text().catch(() => \"\");\n throw new Error(\n `GitHub request failed ${res.status} ${res.statusText}: ${detail.slice(0, 500)}`,\n );\n }\n if (res.status === 204) return undefined as T;\n return (await res.json()) as T;\n}\n\nasync function findExistingComment(input: {\n token: string;\n owner: string;\n repo: string;\n issue: string;\n}): Promise<GitHubComment | null> {\n for (let page = 1; ; page += 1) {\n const comments = await githubRequest<GitHubComment[]>(\n input.token,\n `/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(\n input.repo,\n )}/issues/${encodeURIComponent(input.issue)}/comments?per_page=100&page=${page}`,\n );\n const match = comments.find(\n (comment) =>\n comment.user?.type === \"Bot\" &&\n typeof comment.body === \"string\" &&\n comment.body.includes(MARKER),\n );\n if (match) return match;\n if (comments.length < 100) return null;\n }\n}\n\nasync function upsertComment(input: {\n token: string;\n owner: string;\n repo: string;\n issue: string;\n body: string;\n /** When true, refresh an existing comment but never create a new one. */\n updateOnly?: boolean;\n}): Promise<{\n action: \"created\" | \"updated\" | \"skipped\";\n id: number;\n html_url?: string;\n}> {\n const body = input.body.includes(MARKER)\n ? input.body\n : `${MARKER}\\n${input.body}`;\n const existing = await findExistingComment(input);\n if (!existing && input.updateOnly) {\n // Nothing to refresh and we were told not to create — e.g. a tiny diff with\n // no prior recap. Stay silent rather than posting a \"skipped\" comment.\n return { action: \"skipped\", id: 0 };\n }\n if (existing) {\n const updated = await githubRequest<GitHubComment>(\n input.token,\n `/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(\n input.repo,\n )}/issues/comments/${existing.id}`,\n {\n method: \"PATCH\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ body }),\n },\n );\n return { action: \"updated\", id: existing.id, html_url: updated.html_url };\n }\n const created = await githubRequest<GitHubComment>(\n input.token,\n `/repos/${encodeURIComponent(input.owner)}/${encodeURIComponent(\n input.repo,\n )}/issues/${encodeURIComponent(input.issue)}/comments`,\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ body }),\n },\n );\n return { action: \"created\", id: created.id, html_url: created.html_url };\n}\n\nfunction planIdFromUrl(url: string): string | null {\n // Accept both /recaps/<id> (the canonical recap route the agent now writes)\n // and /plans/<id> (legacy URLs) so the sticky-comment rebuild keeps working.\n const match = url.match(/\\/(?:recaps|plans)\\/([A-Za-z0-9_-]+)/);\n return match ? match[1] : null;\n}\n\n/** True when both URLs parse and share an origin. */\nfunction sameOrigin(a: string, b: string): boolean {\n try {\n return new URL(a).origin === new URL(b).origin;\n } catch {\n return false;\n }\n}\n\n/** The origin of a URL, or \"\" if it doesn't parse. */\nfunction originOf(url: string): string {\n try {\n return new URL(url).origin;\n } catch {\n return \"\";\n }\n}\n\n/** Build the sticky comment body from the workflow's environment. */\nexport function buildCommentBody(env: NodeJS.ProcessEnv = process.env): string {\n const headShort = (env.HEAD_SHA || \"\").slice(0, 7);\n const lines: string[] = [MARKER];\n\n if (env.SUPPRESSED === \"true\") {\n let reason = \"potential secret in diff\";\n try {\n const parsed = JSON.parse(env.SUPPRESSED_JSON || \"{}\");\n if (parsed && typeof parsed.reason === \"string\") reason = parsed.reason;\n } catch {\n /* keep default */\n }\n lines.push(\"### Visual recap — not generated\");\n lines.push(\"\");\n lines.push(\n \"The recap was **suppressed** because the diff matched a secret/credential pattern. No plan was published.\",\n );\n lines.push(\"\");\n lines.push(`Reason: \\`${reason}\\`. Updated for \\`${headShort}\\`.`);\n return lines.join(\"\\n\");\n }\n\n // Tiny diffs aren't worth a recap. Refresh an existing sticky comment to this\n // state (the workflow only updates, never creates, on tiny) so it never lingers\n // pointing at a stale head SHA.\n if (env.DIFF_TINY === \"true\") {\n lines.push(\"### Visual recap — skipped (diff too small)\");\n lines.push(\"\");\n lines.push(\n \"The change in this push is too small to be worth a visual recap. This is informational only and does **not** block the PR.\",\n );\n lines.push(\"\");\n lines.push(`Updated for \\`${headShort}\\`.`);\n return lines.join(\"\\n\");\n }\n\n const planUrl = (env.PLAN_URL || \"\").trim();\n const appUrl = (env.PLAN_RECAP_APP_URL || \"\").trim();\n // recap-url.txt is agent-written → untrusted. Rebuild a canonical link from a\n // TRUSTED base (the configured PLAN_RECAP_APP_URL when set, else the parsed\n // origin of the plan URL) plus a strictly-validated plan id, instead of\n // embedding the raw URL. That both enforces the app origin and prevents\n // markdown injection — a same-origin URL with a crafted path/query could\n // otherwise break out of the markdown link.\n const planId = planUrl ? planIdFromUrl(planUrl) : null;\n const sameOriginOk = appUrl === \"\" || sameOrigin(planUrl, appUrl);\n const base = (appUrl || originOf(planUrl)).replace(/\\/$/, \"\");\n const safeUrl =\n planId && base && sameOriginOk ? `${base}/recaps/${planId}` : \"\";\n if (!safeUrl) {\n lines.push(\"### Visual recap — generation failed\");\n lines.push(\"\");\n lines.push(\n \"The visual recap could not be generated for this push. This is informational only and does **not** block the PR.\",\n );\n lines.push(\"\");\n lines.push(`Updated for \\`${headShort}\\`.`);\n return lines.join(\"\\n\");\n }\n\n // The image URL is produced by our own recap-image route, but validate it is\n // same-origin and matches the canonical hex-token path before embedding it, so\n // it likewise cannot inject markdown.\n const imageUrlRaw = (env.RECAP_IMAGE_URL || \"\").trim();\n const imageUrl =\n imageUrlRaw &&\n sameOrigin(imageUrlRaw, base) &&\n /\\/_agent-native\\/recap-image\\/[0-9a-f]+\\.png$/.test(imageUrlRaw)\n ? imageUrlRaw\n : \"\";\n lines.push(\"### Visual recap — review at a higher altitude\");\n lines.push(\"\");\n if (imageUrl) {\n lines.push(`[](${safeUrl})`);\n lines.push(\"\");\n }\n lines.push(`**[Open the interactive recap](${safeUrl})**`);\n if (env.DIFF_HUGE === \"true\") {\n lines.push(\"\");\n lines.push(\n \"> Large diff — this recap is a **summarized** view (top files + schema/API deltas).\",\n );\n }\n lines.push(\"\");\n lines.push(`Updated for \\`${headShort}\\`.`);\n lines.push(\"\");\n lines.push(`<!-- plan-id: ${planId} -->`);\n return lines.join(\"\\n\");\n}\n\n/* -------------------------------------------------------------------------- */\n/* Subcommands */\n/* -------------------------------------------------------------------------- */\n\nfunction runScan(args: Record<string, string | boolean>): void {\n const diffPath = stringArg(args, \"diff\");\n const diffText = fs.readFileSync(path.resolve(diffPath), \"utf8\");\n if (diffContainsSecret(diffText)) {\n process.stdout.write(\n `${JSON.stringify({ suppressed: true, reason: \"potential secret in diff\" })}\\n`,\n );\n } else {\n process.stdout.write(`${JSON.stringify({ suppressed: false })}\\n`);\n }\n}\n\nfunction runBuildPrompt(args: Record<string, string | boolean>): void {\n const skill = readRepoSkillMd();\n const prompt = buildRecapPrompt({\n skillMd: skill.text,\n pr: stringArg(args, \"pr\"),\n head: optionalArg(args, \"head\"),\n appUrl: optionalArg(args, \"app-url\") ?? \"https://plan.agent-native.com\",\n diffPath: optionalArg(args, \"diff\") ?? \"recap.diff\",\n statPath: optionalArg(args, \"stat\"),\n prevPlanId: optionalArg(args, \"prev-plan-id\"),\n huge: args.huge === true || args.huge === \"true\",\n });\n const out = optionalArg(args, \"out\") ?? \"recap-prompt.md\";\n fs.writeFileSync(path.resolve(out), prompt);\n process.stdout.write(\n `${JSON.stringify({ ok: true, out, skillSource: skill.source, bytes: prompt.length })}\\n`,\n );\n}\n\n/** Upload a PNG to the plan app's signed public image route; returns its URL. */\nasync function uploadRecapImage(input: {\n appUrl: string;\n token: string;\n pngPath: string;\n}): Promise<string | null> {\n try {\n const base = input.appUrl.replace(/\\/$/, \"\");\n const bytes = fs.readFileSync(path.resolve(input.pngPath));\n const res = await fetch(`${base}/_agent-native/recap-image`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"image/png\",\n authorization: `Bearer ${input.token}`,\n },\n body: bytes,\n });\n // Surface failures on stderr — stdout carries the machine-readable JSON the\n // workflow parses, so it must stay clean. A silent null here is exactly what\n // made the missing-inline-thumbnail failure undebuggable from CI logs.\n if (!res.ok) {\n const detail = await res.text().catch(() => \"\");\n process.stderr.write(\n `[recap shot] image upload failed: ${res.status} ${res.statusText} ${detail.slice(0, 300)}\\n`,\n );\n return null;\n }\n const json = (await res.json().catch(() => null)) as {\n imageUrl?: string;\n } | null;\n if (!json?.imageUrl) {\n process.stderr.write(\n `[recap shot] image upload returned no imageUrl (status ${res.status})\\n`,\n );\n return null;\n }\n return json.imageUrl;\n } catch (err) {\n process.stderr.write(`[recap shot] image upload error: ${String(err)}\\n`);\n return null;\n }\n}\n\nasync function runShot(args: Record<string, string | boolean>): Promise<void> {\n const url = stringArg(args, \"url\");\n const out = optionalArg(args, \"out\") ?? \"recap.png\";\n const token = optionalArg(args, \"token\");\n const appUrl = optionalArg(args, \"app-url\");\n\n const done = (obj: Record<string, unknown>) => {\n process.stdout.write(`${JSON.stringify(obj)}\\n`);\n };\n\n // recap-url.txt is produced by the (LLM) agent, so the URL is untrusted. Only\n // forward the reusable publish token to the trusted plan-app origin — never to\n // an arbitrary URL — so a poisoned recap-url.txt can't exfiltrate the bearer\n // to an attacker-controlled domain.\n let attachToken = false;\n if (token) {\n try {\n attachToken = !!appUrl && new URL(url).origin === new URL(appUrl).origin;\n } catch {\n attachToken = false;\n }\n if (!attachToken) {\n done({\n ok: false,\n reason: appUrl\n ? `refusing to screenshot ${url}: origin does not match --app-url (${appUrl}); the publish token is only sent to the trusted plan app origin`\n : `refusing to attach the publish token without --app-url to validate ${url} against`,\n });\n return;\n }\n }\n\n let chromium: typeof import(\"playwright\").chromium | undefined;\n try {\n ({ chromium } = await import(\"playwright\"));\n } catch {\n try {\n ({ chromium } =\n (await import(\"@playwright/test\")) as unknown as typeof import(\"playwright\"));\n } catch (err) {\n done({ ok: false, reason: `playwright not available: ${String(err)}` });\n return;\n }\n }\n\n let captured = false;\n let browser: import(\"playwright\").Browser | undefined;\n const hardTimer = setTimeout(() => {\n done({ ok: false, reason: \"hard 60s timeout reached\" });\n process.exit(0);\n }, 60_000);\n try {\n browser = await chromium!.launch({ args: [\"--no-sandbox\"] });\n const context = await browser.newContext({\n viewport: { width: 1450, height: 1450 },\n deviceScaleFactor: 2,\n });\n if (attachToken) {\n // Attach the bearer ONLY to same-origin requests. Context-wide\n // extraHTTPHeaders would also send it to every cross-origin subresource\n // the plan page loads (CDN images/fonts/scripts), leaking the publish\n // token; routing scopes it to the trusted app origin.\n const appOrigin = new URL(appUrl as string).origin;\n await context.route(\"**/*\", async (route) => {\n const request = route.request();\n if (new URL(request.url()).origin === appOrigin) {\n await route.continue({\n headers: { ...request.headers(), authorization: `Bearer ${token}` },\n });\n } else {\n await route.continue();\n }\n });\n }\n const page = await context.newPage();\n await page.goto(url, { waitUntil: \"networkidle\", timeout: 45_000 });\n const selectors = [\n \"[data-plan-document]\",\n \"[data-plan-block]\",\n \"main article\",\n \"[data-testid='plan-document']\",\n \"main\",\n ];\n let matched = false;\n for (const sel of selectors) {\n try {\n await page.waitForSelector(sel, { timeout: 6_000, state: \"visible\" });\n matched = true;\n break;\n } catch {\n /* try the next selector */\n }\n }\n await page.waitForTimeout(matched ? 1_200 : 500);\n // Zoom out slightly so more content fits. Keep the plan title (h1) in frame:\n // the recap reads better led by its own title than cropped to the body.\n await page.evaluate(() => {\n (document.documentElement as HTMLElement).style.zoom = \"80%\";\n });\n await page.screenshot({ path: out });\n captured = true;\n await browser.close();\n } catch (err) {\n clearTimeout(hardTimer);\n try {\n if (browser) await browser.close();\n } catch {\n /* ignore */\n }\n done({\n ok: false,\n reason: err instanceof Error ? err.message : String(err),\n });\n return;\n }\n clearTimeout(hardTimer);\n\n let imageUrl: string | null = null;\n if (captured && token && appUrl) {\n imageUrl = await uploadRecapImage({ appUrl, token, pngPath: out });\n }\n done({ ok: captured, out, imageUrl });\n}\n\nasync function runComment(\n args: Record<string, string | boolean>,\n sub: string,\n): Promise<void> {\n const token = stringArg(args, \"token\");\n const { owner, repo } = repoParts(stringArg(args, \"repo\"));\n const issue = stringArg(args, \"issue\");\n\n if (sub === \"find-plan-id\") {\n const existing = await findExistingComment({ token, owner, repo, issue });\n const body = existing?.body ?? \"\";\n const match = body.match(/<!--\\s*plan-id:\\s*([^\\s]+)\\s*-->/);\n process.stdout.write(match ? match[1] : \"\");\n return;\n }\n\n if (sub === \"upsert\") {\n const result = await upsertComment({\n token,\n owner,\n repo,\n issue,\n body: buildCommentBody(),\n updateOnly:\n args[\"update-only\"] === true || args[\"update-only\"] === \"true\",\n });\n process.stdout.write(`${JSON.stringify(result)}\\n`);\n return;\n }\n\n throw new Error(\n \"Usage: agent-native recap comment <find-plan-id|upsert> --repo owner/name --issue n --token token\",\n );\n}\n\nconst HELP = `agent-native recap — PR visual recap helpers (used by the GitHub Action)\n\nUsage:\n agent-native recap scan --diff <path>\n agent-native recap build-prompt --pr <n> [--head <sha>] [--app-url <url>] [--diff <path>] [--stat <path>] [--prev-plan-id <id>] [--huge] [--out <path>]\n agent-native recap shot --url <planUrl> [--token <planToken>] [--app-url <url>] [--out recap.png]\n agent-native recap comment <find-plan-id|upsert> --repo owner/name --issue <n> --token <github-token>\n`;\n\nexport async function runRecap(argv: string[]): Promise<void> {\n const [sub, ...rest] = argv;\n const args = parseArgs(rest);\n switch (sub) {\n case \"scan\":\n runScan(args);\n return;\n case \"build-prompt\":\n runBuildPrompt(args);\n return;\n case \"shot\":\n await runShot(args);\n return;\n case \"comment\":\n await runComment(parseArgs(rest.slice(1)), rest[0] ?? \"\");\n return;\n case \"help\":\n case \"--help\":\n case \"-h\":\n case undefined:\n process.stdout.write(HELP);\n return;\n default:\n process.stderr.write(`Unknown recap subcommand: ${sub}\\n${HELP}`);\n process.exit(1);\n }\n}\n"]}
|