Verified workflow checkout behavior.
UnderstandAnalyzed comment trust boundaries and issue deduplication.
UnderstandIdentified risks in comment authentication and concurrency.
UnderstandReview this new GitHub Actions workflow and its two supporting Python scripts for security and correctness bugs. This workflow is meant to run with NO secrets at all — it only reads already-posted (already-redacted) PR review comments and a repo-local archive file, then decides whether to open a GitHub issue. It must never be able to merge, deploy, or exfiltrate anything.
Focus specifically on:
Output findings in the format: N. [P1|P2] description (one line each). If nothing found, say so plainly. ---WORKFLOW--- name: Archive and Recommend
workflow_run, not pull_request or workflow_run'son: workflow_run: workflows: ["Codex Review"] types: [completed]
permissions: contents: read issues: write pull-requests: read
concurrency: group: archive-and-recommend-${{ github.event.workflow_run.id }} cancel-in-progress: false
jobs: analyze: name: Analyze review for recurring patterns runs-on: ubuntu-latest if: github.event.workflow_run.pull_requests[0] != null steps: - name: Checkout (default branch — trusted) uses: actions/checkout@v4 with: persist-credentials: false
- name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12"
- name: Fetch latest Codex review comment for the PR id: fetch-comment uses: actions/github-script@v7 with: script: | const prNumber = context.payload.workflow_run.pull_requests[0].number; const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, per_page: 100, }); const reviewComments = comments.filter((c) => (c.body || "").startsWith("### Codex independent review") ); if (reviewComments.length === 0) { core.setOutput("found", "false"); return; } const latest = reviewComments[reviewComments.length - 1]; const fs = require("fs"); fs.writeFileSync(process.env.RUNNER_TEMP + "/review-comment.txt", latest.body, "utf8"); core.setOutput("found", "true"); core.setOutput("pr-number", String(prNumber));
- name: Analyze for newly-crossed recurrence thresholds id: analyze if: steps.fetch-comment.outputs.found == 'true' run: | python3 scripts/analyze-latest-review.py \ docs/self-improvement-archive.jsonl \ "$RUNNER_TEMP/review-comment.txt" \ > "$RUNNER_TEMP/analysis.txt" cat "$RUNNER_TEMP/analysis.txt" # Extract just the JSON payload after the `---` separator. awk '/^---$/{found=1; next} found' "$RUNNER_TEMP/analysis.txt" > "$RUNNER_TEMP/analysis.json" cat "$RUNNER_TEMP/analysis.json"
- name: Open a tracking issue for each newly-crossed topic if: steps.fetch-comment.outputs.found == 'true' uses: actions/github-script@v7 with: script: | const fs = require("fs"); const prNumber = "${{ steps.fetch-comment.outputs.pr-number }}"; const raw = fs.readFileSync(process.env.RUNNER_TEMP + "/analysis.json", "utf8"); const { newly_crossed: newlyCrossed } = JSON.parse(raw);
if (!newlyCrossed || newlyCrossed.length === 0) { console.log("No topic newly crossed the mechanism-fix threshold. Nothing to do."); return; }
for (const item of newlyCrossed) { const title = `Recurring pattern: ${item.topic} — mechanism-level fix recommended`;
// Idempotency: don't open a second issue for a topic that // already has an open tracking issue. const existing = await github.paginate(github.rest.issues.listForRepo, { owner: context.repo.owner, repo: context.repo.repo, state: "open", labels: "self-improvement-recommendation", per_page: 100, }); if (existing.some((issue) => issue.title === title)) { console.log(`Issue already open for topic "${item.topic}" — skipping.`); continue; }
const body = [ `Automatically opened by [\`archive-and-recommend.yml\`](../blob/main/.github/workflows/archive-and-recommend.yml).`, ``, `A finding topic newly crossed the mechanism-level-fix recurrence`, `threshold in \`docs/self-improvement-archive.jsonl\`, as of the`, `Codex review on PR #${prNumber}.`, ``, `**Topic:** \`${item.topic}\``, `**Recommended action:** ${item.recommended_action}-level fix`, ``, `Run \`python3 scripts/detect-recurring-pattern.py docs/self-improvement-archive.jsonl\``, `for the full evidence trail (which rounds, which findings) behind this`, `recommendation.`, ``, `This issue was opened automatically. Deciding what the mechanism-level`, `fix should be, and merging/deploying it, remains a human decision —`, `this workflow's only permission is \`issues: write\`.`, ].join("\n");
await github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, title, body, labels: ["self-improvement-recommendation"], }); console.log(`Opened tracking issue for topic "${item.topic}".`); }---analyze-latest-review.py--- #!/usr/bin/env python3 """Decide, from evidence, whether a just-completed review round newly crosses the mechanism-fix threshold for any finding topic.
This is the piece that closes the gap named while building this archive: scripts/detect-recurring-pattern.py could already derive a target-vs- mechanism recommendation from the archive's accumulated data, but something still had to run it and decide whether the result was worth acting on -- that was a human/agent judgment call, made by eyeballing the tool's output.
This script makes that specific decision mechanical: it compares the recommendation with vs. without the latest round's findings included, and reports only topics whose recommendation flips from "target" to "mechanism" (or newly appears at/above threshold) because of this round specifically -- not topics that already crossed the threshold in earlier rounds, which would otherwise fire on every single subsequent round forever. A workflow can run this automatically after every review and act (e.g. open a tracking issue) purely on its output, with no one needing to have read the archive and noticed the pattern themselves.
Usage: python3 analyze-latest-review.py <archive.jsonl> <review-comment.txt> [--threshold N]
Exits 0 always (advisory). Prints newline-delimited human-readable lines,
then a --- separator, then a JSON object: {"newly_crossed": [...]}.
"""
from future import annotations
import argparse import importlib.util import json import sys from pathlib import Path
def _load_sibling_module(name: str, filename: str): path = Path(file).parent / filename spec = importlib.util.spec_from_file_location(name, path) assert spec is not None and spec.loader is not None module = importlib.util.module_from_spec(spec) sys.modules[name] = module spec.loader.exec_module(module) return module
detect = _load_sibling_module("detect_recurring_pattern", "detect-recurring-pattern.py") parse_findings_mod = _load_sibling_module("parse_review_findings", "parse-review-findings.py")
def load_archive(path: str) -> list[dict]: entries = [] with open(path) as f: for line in f: line = line.strip() if line: entries.append(json.loads(line)) return entries
def next_round_number(entries: list[dict]) -> int: rounds = [e.get("round", 0) for e in entries] return (max(rounds) + 1) if rounds else 1
def recommendations_by_topic(entries: list[dict], threshold: int) -> dict[str, str]: result = detect.analyze(entries, threshold) return {rec["topic"]: rec["recommended_action"] for rec in result["recommendations"]}
def find_newly_crossed_topics( archive_entries: list[dict], new_findings: list[str], threshold: int ) -> list[dict]: """Compare recommendations with vs. without the new round's findings.
Returns entries for topics that recommend "mechanism" only once the newround is included -- i.e. this round is the one that tipped it over,not a topic that already exceeded the threshold in prior rounds."""before = recommendations_by_topic(archive_entries, threshold)
new_round_entry = {"round": next_round_number(archive_entries), "findings": new_findings}after_entries = [*archive_entries, new_round_entry]after = recommendations_by_topic(after_entries, threshold)
newly_crossed = []for topic, action in after.items(): if action != "mechanism": continue was_mechanism_before = before.get(topic) == "mechanism" if not was_mechanism_before: newly_crossed.append({"topic": topic, "recommended_action": action})
return newly_crosseddef main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=doc) parser.add_argument("archive_path") parser.add_argument("review_comment_path") parser.add_argument("--threshold", type=int, default=detect.DEFAULT_THRESHOLD) args = parser.parse_args(argv[1:])
archive_entries = load_archive(args.archive_path)
with open(args.review_comment_path) as f: comment_text = f.read()new_findings = parse_findings_mod.parse_findings(comment_text)
if not new_findings: print("No findings in the latest review — nothing to analyze.") print("---") print(json.dumps({"newly_crossed": []}, indent=2)) return 0
newly_crossed = find_newly_crossed_topics(archive_entries, new_findings, args.threshold)
if newly_crossed: for item in newly_crossed: print( f"[{item['topic']}] newly recommends a MECHANISM-LEVEL fix " f"as of this round's findings." )else: print("No topic newly crosses the mechanism-fix threshold this round.")
print("---")print(json.dumps({"newly_crossed": newly_crossed}, indent=2))return 0if name == "main": sys.exit(main(sys.argv)) ---detect-recurring-pattern.py (analyze function)--- #!/usr/bin/env python3 """Decide target-fix vs. mechanism-fix from the improvement archive itself.
Closes the specific gap named while building docs/self-improvement-archive.jsonl: across rounds 1-5, I (the agent) judged when a finding was serious/recurring enough to warrant revising the improvement mechanism itself (round 5) rather than just patching the current target again (rounds 2-4). That judgment call was mine, not something derived from the archive's own data — which is exactly the gap the paper's L5 definition ("persistently revises a mechanism that governs subsequent improvement") requires closing: the recursion has to include deciding when to recurse on the mechanism, not just executing that decision once someone notices a pattern.
This script makes that decision algorithmically instead: it reads the archive, buckets every finding into a topic by keyword co-occurrence (no ML, no external calls -- deliberately simple and auditable), and recommends "mechanism" once a topic has recurred at or above a threshold across distinct rounds, "target" otherwise. It is still a human (or an agent executing on the human's behalf) who reads the recommendation and acts on it -- this does not make merge/deploy autonomous, and does not claim to. What it closes is narrower and real: the decision itself is now reproducible from evidence, not from an agent's unrecorded judgment.
Usage: python3 detect-recurring-pattern.py <archive.jsonl> [--threshold N]
Prints one recommendation line per topic that has reached the threshold,
plus a machine-readable JSON summary to stdout after a --- separator.
Exit code 0 always (this is advisory, not a pass/fail gate).
"""
from future import annotations
import argparse import json import sys from collections import defaultdict
DEFAULT_THRESHOLD = 3
TOPIC_KEYWORDS: dict[str, list[str]] = { "credential-redaction": ["redact", "credential", "secret", "token", "leak", "expos"], "shell-semantics": ["errexit", "bash -e", "exit code", "-e", "pipefail", "shell"], "env-var-precedence": ["precedence", "env var", "environment variable", "unconditionally"], "fork-pr-permissions": ["fork", "github_token", "persist-credentials"], "auth-lifecycle": ["refresh token", "rotat", "expir", "auth.json", "stale"], }
def classify_finding(text: str) -> str | None: lowered = text.lower() for topic, keywords in TOPIC_KEYWORDS.items(): if any(kw in lowered for kw in keywords): return topic return None
def load_archive(path: str) -> list[dict]: entries = [] with open(path) as f: for line in f: line = line.strip() if line: entries.append(json.loads(line)) return entries
def analyze(entries: list[dict], threshold: int) -> dict: topic_rounds: dict[str, set[int]] = defaultdict(set) topic_examples: dict[str, list[str]] = defaultdict(list)
for entry in entries: round_num = entry.get("round") for finding in entry.get("findings", []): topic = classify_finding(finding) if topic is None: continue topic_rounds[topic].add(round_num) if len(topic_examples[topic]) < 3: topic_examples[topic].append(f"round {round_num}: {finding[:120]}")
recommendations = []for topic, rounds in sorted(topic_rounds.items(), key=lambda kv: -len(kv[1])): recurrence = len(rounds) action = "mechanism" if recurrence >= threshold else "target" recommendations.append( { "topic": topic, "recurrence_count": recurrence, "rounds": sorted(rounds), "recommended_action": action, "examples": topic_examples[topic], } )
return {"threshold": threshold, "recommendations": recommendations}def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=doc) parser.add_argument("archive_path") parser.add_argument("--threshold", type=int, default=DEFAULT_THRESHOLD) args = parser.parse_args(argv[1:])
entries = load_archive(args.archive_path)result = analyze(entries, args.threshold)
for rec in result["recommendations"]: marker = "MECHANISM-LEVEL FIX RECOMMENDED" if rec["recommended_action"] == "mechanism" else "target-level fix sufficient so far" print( f"[{rec['topic']}] recurred in {rec['recurrence_count']} round(s) " f"{rec['rounds']} -> {marker}" ) for example in rec["examples"]: print(f" - {example}")
print("---")print(json.dumps(result, indent=2))return 0if name == "main": sys.exit(main(sys.argv))
I’ll verify GitHub’s workflow_run checkout behavior and review the comment trust boundary, issue deduplication, and threshold logic. I’m using the agent-reach skill for the documentation lookup.
>cat /Users/gaganarora/.agents/skills/agent-reach/SKILL.md{"chunk_id":"1bff89","wall_time_seconds":0.000004625,"exit_code":0,"original_token_count":1336,"output":"---\nname: agent-reach\ndescription: >\n MUST USE when user wants to research/search/look up/find anything on the\n internet — e.g. \"research this topic\", \"do a deep dive on X\", \"search the\n web for X\", \"see what people say about X\", \"look this up\".\n\n Also MUST USE when user mentions any platform or shares any URL/link:\n Twitter/X, Reddit, Facebook, Instagram, YouTube, GitHub, Bilibili, XiaoHongShu,\n Xiaoyuzhou Podcast, LinkedIn/jobs/recruiting, V2EX, Xueqiu (stocks), RSS.\n\n 15 platforms, multi-backend routing (OpenCLI / per-platform CLIs / APIs).\n Zero config for 6 channels. Run `agent-reach doctor --json` to see which\n backend serves each platform right now.\n\n NOT for: writing reports/analysis/translation (this skill only FETCHES\n internet content); posting/commenting/liking (write operations); platforms\n that already have a dedicated skill installed (prefer that skill).\nmetadata:\n openclaw:\n homepage: https://github.com/Panniantong/Agent-Reach\n---\n\n# Agent Reach — internet capability router\n\n15 platforms, multiple backends each. **When this skill exists, use it for\nthese platforms — do not invent your own approach.**\n\n## Standing rules (apply for the whole session)\n\n1. **Health-check before acting**: for multi-backend/login-backed platforms (XiaoHongShu /\n Reddit / Bilibili / Twitter / Facebook / Instagram), run `agent-reach doctor --json` first and\n pick the command group matching each platform's `active_backend`.\n2. **Announce what you use**: say \"using agent-reach, platform X via backend Y\"\n before starting.\n3. **On failure, follow the retry chains in references/** — never guess\n commands.\n4. **For broad research tasks**: combine platforms (Exa for web search +\n Twitter/Reddit for discussions + XiaoHongShu/Bilibili for Chinese\n perspectives), collect in parallel, then synthesize.\n5. **Watch versions for the user**: after finishing a substantial\n multi-platform task, run `agent-reach check-update` (fast, one API call).\n If a new version exists, append one line to your wrap-up: \"Agent Reach\n vX.Y.Z is available — paste this to me to update: 帮我更新 Agent Reach:\n https://raw.githubusercontent.com/Panniantong/agent-reach/main/docs/update.md\".\n Never interrupt the current task to update; never nag about the same version twice.\n\n## Routing table\n\n| User intent | Category | Details |\n|---------|------|---------|\n| Web / code search | search | [references/search.md](references/search.md) |\n| XiaoHongShu / Twitter / Bilibili / V2EX / Reddit / Facebook / Instagram | social | [references/social.md](references/social.md) |\n| Jobs / LinkedIn | career | [references/career.md](references/career.md) |\n| GitHub / code | dev | [references/dev.md](references/dev.md) |\n| Web pages / articles / RSS | web | [references/web.md](references/web.md) |\n| YouTube / Bilibili / podcast transcripts | video | [references/video.md](references/video.md) |\n\n## Zero-config quick commands\n\n```bash\n# Exa web search\nmcporter call 'exa.web_search_exa(query: \"query\", numResults: 5)'\n\n# Read any web page\ncurl -s \"https://r.jina.ai/URL\"\n\n# GitHub search\ngh search repos \"query\" --sort stars --limit 10\n\n# YouTube subtitles (NOTE: never use yt-dlp for Bilibili — see video.md)\nyt-dlp --write-sub --skip-download -o \"/tmp/%(id)s\" \"URL\"\n\n# V2EX hot topics\ncurl -s \"https://www.v2ex.com/api/topics/hot.json\" -H \"User-Agent: agent-reach/1.0\"\n\n# Bilibili search (bili-cli, no login needed)\nbili search \"query\" --type video -n 5\n```\n\n## Login-backed platforms (pick by doctor's active_backend)\n\n```bash\n# Twitter search (twitter-cli preferred; retry chain in social.md)\ntwitter search \"query\" -n 10\n\n# Reddit (NO zero-config path — OpenCLI or rdt-cli, login required)\nopencli reddit search \"query\" -f yaml # desktop\nrdt search \"query\" --limit 10 # legacy/server\n\n# XiaoHongShu (desktop prefers OpenCLI)\nopencli xiaohongshu search \"query\" -f yaml\n\n# Facebook / Instagram (desktop OpenCLI, browser session)\nopencli facebook search \"query\" -f yaml\nopencli facebook groups -f yaml\nopencli instagram search \"query\" -f yaml # user search\nopencli instagram user USERNAME -f yaml # recent posts from one user\n```\n\n## Environment check\n\n```bash\n# Channel availability + which backend serves each platform\nagent-reach doctor --json\n```\n\n## Workspace rules\n\n**Never create files in the agent workspace.** Use `/tmp/` for temporary\noutput and `~/.agent-reach/` for persistent data.\n\n## Detailed references\n\nRead the matching file when you need specifics (commands above cover the\ncommon cases; references hold per-backend command groups, caveats, retry\nchains — note: reference docs are written in Chinese, commands are universal):\n\n- [Search](references/search.md) — Exa AI search\n- [Social](references/social.md) — XiaoHongShu, Twitter, Bilibili, V2EX, Reddit, Facebook, Instagram (multi-backend/login-backed groups)\n- [Career](references/career.md) — LinkedIn\n- [Dev](references/dev.md) — GitHub CLI\n- [Web](references/web.md) — Jina Reader, RSS\n- [Video](references/video.md) — YouTube, Bilibili, Xiaoyuzhou\n\n## Configure a channel\n\nIf a channel needs setup, fetch the install guide:\nhttps://raw.githubusercontent.com/Panniantong/agent-reach/main/docs/install.md\n\nThe user only provides cookies / one extension click; the agent does the rest.\n"}
[{"name":"list_mcp_resource_templates","description":"Lists resource templates provided by MCP servers. Parameterized resource templates allow s"},{"name":"list_mcp_resources","description":"Lists resources provided by MCP servers. Resources allow servers to share data that provid"},{"name":"mcp__codex_apps__codex_document_control_get_document_tool_schemas","description":"Use Codex Document Control to find connected document sessions, inspect the tools supporte"},{"name":"mcp__codex_apps__github_create_file","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_delete_file","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_download_user_content","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_blob","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_commit","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_commit_workflow_runs","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_file","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_issue","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_issue_comments","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_pr","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_pr_comments","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_pr_file_patch","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_pr_patch","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_workflow_job_logs","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_workflow_job_steps","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_workflow_run_artifacts","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_workflow_run_jobs","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_commit_combined_status","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_issue_comment_reactions","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_pr_diff","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_pr_info","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_pr_reactions","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_pr_review_comment_reactions","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_users_recent_prs_in_repo","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_list_repositories","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_branches","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_commits","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_installed_repositories_streaming","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_installed_repositories_v2","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_issues","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_prs","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_repositories","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_update_file","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__gmail_apply_labels_to_emails","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_archive_emails","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_batch_modify_email","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_batch_read_email","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_batch_read_email_threads","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_bulk_label_matching_emails","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_create_draft","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_create_label","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_delete_emails","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_forward_emails","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_get_profile","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_list_drafts","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_list_labels","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_read_attachment","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_read_email","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_read_email_thread","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_search_email_ids","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_search_emails","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_send_draft","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_send_email","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_update_draft","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__google_calendar_batch_read_event","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_create_event","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_delete_event","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_fetch","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_get_availability","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_get_colors","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_get_profile","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_list_calendars","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_list_event_labels","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_read_event","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_respond_event","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_search","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_search_events","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_set_event_label_silently","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_update_event","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_drive_batch_update_document","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nApply raw Google "},{"name":"mcp__codex_apps__google_drive_batch_update_presentation","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nApply raw Google "},{"name":"mcp__codex_apps__google_drive_batch_update_spreadsheet","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nApply raw Google "},{"name":"mcp__codex_apps__google_drive_bulk_update_file_comments","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nCreate, reply to,"},{"name":"mcp__codex_apps__google_drive_copy_file","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nCopy a Drive file"},{"name":"mcp__codex_apps__google_drive_create_file","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nCreate a native G"},{"name":"mcp__codex_apps__google_drive_create_folder","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nCreate a folder i"},{"name":"mcp__codex_apps__google_drive_create_presentation_from_template","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nCopy a Google Sli"},{"name":"mcp__codex_apps__google_drive_delete_file","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nPermanently delet"},{"name":"mcp__codex_apps__google_drive_duplicate_sheet_in_new_spreadsheet","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nDuplicate an exis"},{"name":"mcp__codex_apps__google_drive_export_file","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nExport a native G"},{"name":"mcp__codex_apps__google_drive_fetch","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nWith default opti"},{"name":"mcp__codex_apps__google_drive_fetch_file_revision","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nFetch text and re"},{"name":"mcp__codex_apps__google_drive_find_document_text_range","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nFind the index ra"},{"name":"mcp__codex_apps__google_drive_get_document","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nGet a native Goog"},{"name":"mcp__codex_apps__google_drive_get_document_comments","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nRead user comment"}]
{"chunk_id":"b4df55","wall_time_seconds":0.000084458,"exit_code":0,"original_token_count":165,"output":"AGENTS.md\ndocs/self-improvement-archive.jsonl\nscripts/analyze_latest_review_test.py\nscripts/analyze-latest-review.py\nscripts/detect-recurring-pattern.py\nscripts/parse-review-findings.py\nscripts/detect_recurring_pattern_test.py\nscripts/parse_review_findings_test.py\nCONTRIBUTING.md\npackages/web/src/components/attachment-preview-strip.tsx\npackages/web/src/components/settings/skills-settings/skill-import-review.tsx\npackages/web/src/components/settings/skills-settings/use-import-preview.ts\npackages/web/src/components/settings/skills-settings/skill-import-review.test.tsx\npackages/web/src/components/settings/skills-settings/skill-import-preview-race.test.tsx\n"}>rg --files -g '*review*' -g '*recurring*' -g '*improvement*' -g AGENTS.md -g CONTRIBUTING.md{"chunk_id":"1bff89","wall_time_seconds":0.000004625,"exit_code":0,"original_token_count":1336,"output":"---\nname: agent-reach\ndescription: >\n MUST USE when user wants to research/search/look up/find anything on the\n internet — e.g. \"research this topic\", \"do a deep dive on X\", \"search the\n web for X\", \"see what people say about X\", \"look this up\".\n\n Also MUST USE when user mentions any platform or shares any URL/link:\n Twitter/X, Reddit, Facebook, Instagram, YouTube, GitHub, Bilibili, XiaoHongShu,\n Xiaoyuzhou Podcast, LinkedIn/jobs/recruiting, V2EX, Xueqiu (stocks), RSS.\n\n 15 platforms, multi-backend routing (OpenCLI / per-platform CLIs / APIs).\n Zero config for 6 channels. Run `agent-reach doctor --json` to see which\n backend serves each platform right now.\n\n NOT for: writing reports/analysis/translation (this skill only FETCHES\n internet content); posting/commenting/liking (write operations); platforms\n that already have a dedicated skill installed (prefer that skill).\nmetadata:\n openclaw:\n homepage: https://github.com/Panniantong/Agent-Reach\n---\n\n# Agent Reach — internet capability router\n\n15 platforms, multiple backends each. **When this skill exists, use it for\nthese platforms — do not invent your own approach.**\n\n## Standing rules (apply for the whole session)\n\n1. **Health-check before acting**: for multi-backend/login-backed platforms (XiaoHongShu /\n Reddit / Bilibili / Twitter / Facebook / Instagram), run `agent-reach doctor --json` first and\n pick the command group matching each platform's `active_backend`.\n2. **Announce what you use**: say \"using agent-reach, platform X via backend Y\"\n before starting.\n3. **On failure, follow the retry chains in references/** — never guess\n commands.\n4. **For broad research tasks**: combine platforms (Exa for web search +\n Twitter/Reddit for discussions + XiaoHongShu/Bilibili for Chinese\n perspectives), collect in parallel, then synthesize.\n5. **Watch versions for the user**: after finishing a substantial\n multi-platform task, run `agent-reach check-update` (fast, one API call).\n If a new version exists, append one line to your wrap-up: \"Agent Reach\n vX.Y.Z is available — paste this to me to update: 帮我更新 Agent Reach:\n https://raw.githubusercontent.com/Panniantong/agent-reach/main/docs/update.md\".\n Never interrupt the current task to update; never nag about the same version twice.\n\n## Routing table\n\n| User intent | Category | Details |\n|---------|------|---------|\n| Web / code search | search | [references/search.md](references/search.md) |\n| XiaoHongShu / Twitter / Bilibili / V2EX / Reddit / Facebook / Instagram | social | [references/social.md](references/social.md) |\n| Jobs / LinkedIn | career | [references/career.md](references/career.md) |\n| GitHub / code | dev | [references/dev.md](references/dev.md) |\n| Web pages / articles / RSS | web | [references/web.md](references/web.md) |\n| YouTube / Bilibili / podcast transcripts | video | [references/video.md](references/video.md) |\n\n## Zero-config quick commands\n\n```bash\n# Exa web search\nmcporter call 'exa.web_search_exa(query: \"query\", numResults: 5)'\n\n# Read any web page\ncurl -s \"https://r.jina.ai/URL\"\n\n# GitHub search\ngh search repos \"query\" --sort stars --limit 10\n\n# YouTube subtitles (NOTE: never use yt-dlp for Bilibili — see video.md)\nyt-dlp --write-sub --skip-download -o \"/tmp/%(id)s\" \"URL\"\n\n# V2EX hot topics\ncurl -s \"https://www.v2ex.com/api/topics/hot.json\" -H \"User-Agent: agent-reach/1.0\"\n\n# Bilibili search (bili-cli, no login needed)\nbili search \"query\" --type video -n 5\n```\n\n## Login-backed platforms (pick by doctor's active_backend)\n\n```bash\n# Twitter search (twitter-cli preferred; retry chain in social.md)\ntwitter search \"query\" -n 10\n\n# Reddit (NO zero-config path — OpenCLI or rdt-cli, login required)\nopencli reddit search \"query\" -f yaml # desktop\nrdt search \"query\" --limit 10 # legacy/server\n\n# XiaoHongShu (desktop prefers OpenCLI)\nopencli xiaohongshu search \"query\" -f yaml\n\n# Facebook / Instagram (desktop OpenCLI, browser session)\nopencli facebook search \"query\" -f yaml\nopencli facebook groups -f yaml\nopencli instagram search \"query\" -f yaml # user search\nopencli instagram user USERNAME -f yaml # recent posts from one user\n```\n\n## Environment check\n\n```bash\n# Channel availability + which backend serves each platform\nagent-reach doctor --json\n```\n\n## Workspace rules\n\n**Never create files in the agent workspace.** Use `/tmp/` for temporary\noutput and `~/.agent-reach/` for persistent data.\n\n## Detailed references\n\nRead the matching file when you need specifics (commands above cover the\ncommon cases; references hold per-backend command groups, caveats, retry\nchains — note: reference docs are written in Chinese, commands are universal):\n\n- [Search](references/search.md) — Exa AI search\n- [Social](references/social.md) — XiaoHongShu, Twitter, Bilibili, V2EX, Reddit, Facebook, Instagram (multi-backend/login-backed groups)\n- [Career](references/career.md) — LinkedIn\n- [Dev](references/dev.md) — GitHub CLI\n- [Web](references/web.md) — Jina Reader, RSS\n- [Video](references/video.md) — YouTube, Bilibili, Xiaoyuzhou\n\n## Configure a channel\n\nIf a channel needs setup, fetch the install guide:\nhttps://raw.githubusercontent.com/Panniantong/agent-reach/main/docs/install.md\n\nThe user only provides cookies / one extension click; the agent does the rest.\n"}
[{"name":"list_mcp_resource_templates","description":"Lists resource templates provided by MCP servers. Parameterized resource templates allow s"},{"name":"list_mcp_resources","description":"Lists resources provided by MCP servers. Resources allow servers to share data that provid"},{"name":"mcp__codex_apps__codex_document_control_get_document_tool_schemas","description":"Use Codex Document Control to find connected document sessions, inspect the tools supporte"},{"name":"mcp__codex_apps__github_create_file","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_delete_file","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_download_user_content","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_blob","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_commit","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_commit_workflow_runs","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_file","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_issue","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_issue_comments","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_pr","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_pr_comments","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_pr_file_patch","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_pr_patch","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_workflow_job_logs","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_workflow_job_steps","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_workflow_run_artifacts","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_fetch_workflow_run_jobs","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_commit_combined_status","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_issue_comment_reactions","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_pr_diff","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_pr_info","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_pr_reactions","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_pr_review_comment_reactions","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_get_users_recent_prs_in_repo","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_list_repositories","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_branches","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_commits","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_installed_repositories_streaming","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_installed_repositories_v2","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_issues","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_prs","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_search_repositories","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__github_update_file","description":"Access repositories, issues, and pull requests. Required for some features such as Codex\n\n"},{"name":"mcp__codex_apps__gmail_apply_labels_to_emails","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_archive_emails","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_batch_modify_email","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_batch_read_email","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_batch_read_email_threads","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_bulk_label_matching_emails","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_create_draft","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_create_label","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_delete_emails","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_forward_emails","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_get_profile","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_list_drafts","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_list_labels","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_read_attachment","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_read_email","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_read_email_thread","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_search_email_ids","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_search_emails","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_send_draft","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_send_email","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__gmail_update_draft","description":"Gmail tools for label counts, searching and reading emails/threads/attachments, reviewing "},{"name":"mcp__codex_apps__google_calendar_batch_read_event","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_create_event","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_delete_event","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_fetch","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_get_availability","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_get_colors","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_get_profile","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_list_calendars","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_list_event_labels","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_read_event","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_respond_event","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_search","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_search_events","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_set_event_label_silently","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_calendar_update_event","description":"Google Calendar tools for searching/reading events, checking availability before schedulin"},{"name":"mcp__codex_apps__google_drive_batch_update_document","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nApply raw Google "},{"name":"mcp__codex_apps__google_drive_batch_update_presentation","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nApply raw Google "},{"name":"mcp__codex_apps__google_drive_batch_update_spreadsheet","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nApply raw Google "},{"name":"mcp__codex_apps__google_drive_bulk_update_file_comments","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nCreate, reply to,"},{"name":"mcp__codex_apps__google_drive_copy_file","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nCopy a Drive file"},{"name":"mcp__codex_apps__google_drive_create_file","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nCreate a native G"},{"name":"mcp__codex_apps__google_drive_create_folder","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nCreate a folder i"},{"name":"mcp__codex_apps__google_drive_create_presentation_from_template","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nCopy a Google Sli"},{"name":"mcp__codex_apps__google_drive_delete_file","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nPermanently delet"},{"name":"mcp__codex_apps__google_drive_duplicate_sheet_in_new_spreadsheet","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nDuplicate an exis"},{"name":"mcp__codex_apps__google_drive_export_file","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nExport a native G"},{"name":"mcp__codex_apps__google_drive_fetch","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nWith default opti"},{"name":"mcp__codex_apps__google_drive_fetch_file_revision","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nFetch text and re"},{"name":"mcp__codex_apps__google_drive_find_document_text_range","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nFind the index ra"},{"name":"mcp__codex_apps__google_drive_get_document","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nGet a native Goog"},{"name":"mcp__codex_apps__google_drive_get_document_comments","description":"Search and work with files from Google Drive, Docs, Sheets, and Slides.\n\nRead user comment"}]
{"chunk_id":"b4df55","wall_time_seconds":0.000084458,"exit_code":0,"original_token_count":165,"output":"AGENTS.md\ndocs/self-improvement-archive.jsonl\nscripts/analyze_latest_review_test.py\nscripts/analyze-latest-review.py\nscripts/detect-recurring-pattern.py\nscripts/parse-review-findings.py\nscripts/detect_recurring_pattern_test.py\nscripts/parse_review_findings_test.py\nCONTRIBUTING.md\npackages/web/src/components/attachment-preview-strip.tsx\npackages/web/src/components/settings/skills-settings/skill-import-review.tsx\npackages/web/src/components/settings/skills-settings/use-import-preview.ts\npackages/web/src/components/settings/skills-settings/skill-import-review.test.tsx\npackages/web/src/components/settings/skills-settings/skill-import-preview-race.test.tsx\n"}>cat /Users/gaganarora/.agents/skills/agent-reach/references/web.md{"i":0,"result":{"status":"fulfilled","value":{"chunk_id":"b54d27","wall_time_seconds":6.25e-7,"exit_code":0,"original_token_count":276,"output":"# 网页阅读\n\n通用网页、RSS。\n\n## 通用网页 (Jina Reader)\n\n```bash\n# 读取任意网页内容\ncurl -s \"https://r.jina.ai/URL\"\n\n# 示例\ncurl -s \"https://r.jina.ai/https://example.com/article\"\n```\n\n**适用场景**: 大多数网页可以直接用 Jina Reader 读取。\n\n## Web Reader (MCP)\n\n```bash\n# 读取网页内容 (Markdown 格式)\nmcporter call 'web-reader.webReader(url: \"https://example.com\")'\n\n# 保留图片\nmcporter call 'web-reader.webReader(url: \"https://example.com\", retain_images: true)'\n\n# 纯文本格式\nmcporter call 'web-reader.webReader(url: \"https://example.com\", return_format: \"text\")'\n```\n\n**适用场景**: 需要更精确控制输出格式时使用。\n\n## RSS (feedparser)\n\n```python\npython3 -c \"\nimport feedparser\nfor e in feedparser.parse('FEED_URL').entries[:5]:\n print(f'{e.title} — {e.link}')\n\"\n```\n\n**适用场景**: 订阅博客、新闻源、播客等 RSS feed。\n\n## 选择指南\n\n| 场景 | 推荐工具 |\n|-----|---------|\n| 通用网页 | Jina Reader (`curl r.jina.ai`) |\n| 需要图片/格式控制 | web-reader MCP |\n| RSS 订阅 | feedparser |\n"}}}
{"i":1,"result":{"status":"fulfilled","value":{"chunk_id":"8e852e","wall_time_seconds":0.00000275,"exit_code":0,"original_token_count":11279,"output":"#!/usr/bin/env python3\n\"\"\"Parse [P1]/[P2] findings out of a Codex review comment's raw text.\n\nExtracted as its own tested tool because the next piece of this archive\n(the workflow that reads a completed Codex review and decides, on its own,\nwhether the recurrence pattern warrants opening a tracking issue) needs a\nreliable, reusable way to turn free-form review prose back into a list of\nindividual findings -- the same shape docs/self-improvement-archive.jsonl\nalready stores per round.\n\nFindings in this archive's own convention start a line with a number, a\nperiod, and a **[P1]** or **[P2]** marker, e.g.:\n 1. **[P1]** Some critical issue description.\nA finding may span multiple lines until the next numbered marker or the end\nof the text; this parser keeps only the first line to match how findings\nare already recorded in the archive (short, single-line summaries).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nimport sys\n\nFINDING_LINE_RE = re.compile(r\"^\\s*\\d+\\.\\s*(\\*\\*\\[(P1|P2)\\]\\*\\*.*)$\")\n\n\ndef parse_findings(text: str) -> list[str]:\n findings = []\n for line in text.splitlines():\n match = FINDING_LINE_RE.match(line)\n if match:\n findings.append(match.group(1).strip())\n return findings\n\n\ndef main(argv: list[str]) -> int:\n if len(argv) == 2:\n with open(argv[1]) as f:\n text = f.read()\n elif len(argv) == 1:\n text = sys.stdin.read()\n else:\n print(\"usage: parse-review-findings.py [file] (reads stdin if omitted)\", file=sys.stderr)\n return 2\n\n findings = parse_findings(text)\n print(json.dumps(findings, indent=2))\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main(sys.argv))\n\"\"\"Tests for analyze-latest-review.py.\n\nRun with: python3 -m pytest scripts/analyze_latest_review_test.py -q\n\"\"\"\n\nimport importlib.util\nimport json\nimport sys\nfrom pathlib import Path\n\n_MODULE_PATH = Path(__file__).parent / \"analyze-latest-review.py\"\n_spec = importlib.util.spec_from_file_location(\"analyze_latest_review\", _MODULE_PATH)\nassert _spec is not None and _spec.loader is not None\nanalyze_mod = importlib.util.module_from_spec(_spec)\nsys.modules[\"analyze_latest_review\"] = analyze_mod\n_spec.loader.exec_module(analyze_mod)\n\n\ndef _entry(round_num, findings):\n return {\"round\": round_num, \"findings\": findings}\n\n\ndef test_topic_not_yet_crossed_and_new_round_pushes_it_over():\n \"\"\"Two prior rounds mention credential redaction (below threshold 3);\n a new round's findings supply the third -> must be reported as newly\n crossed.\"\"\"\n archive = [\n _entry(1, [\"**[P1]** Secret token leaked in stdout.\"]),\n _entry(2, [\"**[P2]** Credential redaction missed a field.\"]),\n ]\n new_findings = [\"**[P1]** Another secret exposed in stderr.\"]\n\n crossed = analyze_mod.find_newly_crossed_topics(archive, new_findings, threshold=3)\n topics = {c[\"topic\"] for c in crossed}\n assert \"credential-redaction\" in topics\n\n\ndef test_topic_already_at_mechanism_level_is_not_reported_again():\n \"\"\"A topic that already recommended 'mechanism' in the archive alone\n must NOT be reported every subsequent round -- only the round that\n first tips it over counts as 'newly crossed'.\"\"\"\n archive = [\n _entry(1, [\"**[P1]** Secret leaked.\"]),\n _entry(2, [\"**[P2]** Credential redaction missed a field.\"]),\n _entry(3, [\"**[P1]** Token exposed again.\"]),\n ]\n # Already at/above threshold 3 without the new round.\n new_findings = [\"**[P2]** Yet another credential leak.\"]\n\n crossed = analyze_mod.find_newly_crossed_topics(archive, new_findings, threshold=3)\n assert crossed == []\n\n\ndef test_unrelated_new_finding_does_not_falsely_cross():\n archive = [\n _entry(1, [\"**[P1]** Secret leaked.\"]),\n _entry(2, [\"**[P2]** Credential redaction missed a field.\"]),\n ]\n new_findings = [\"**[P2]** Minor typo in a comment.\"]\n\n crossed = analyze_mod.find_newly_crossed_topics(archive, new_findings, threshold=3)\n assert crossed == []\n\n\ndef test_empty_archive_with_new_round_below_threshold_reports_nothing():\n crossed = analyze_mod.find_newly_crossed_topics([], [\"**[P1]** Secret leaked once.\"], threshold=3)\n assert crossed == []\n\n\ndef test_next_round_number_from_empty_archive_is_one():\n assert analyze_mod.next_round_number([]) == 1\n\n\ndef test_next_round_number_increments_from_max():\n archive = [_entry(1, []), _entry(4, []), _entry(2, [])]\n assert analyze_mod.next_round_number(archive) == 5\n\n\ndef test_main_cli_with_real_archive_and_no_findings_in_review(tmp_path, capsys):\n archive_path = tmp_path / \"archive.jsonl\"\n archive_path.write_text(\n json.dumps(_entry(1, [\"**[P1]** Secret leaked.\"])) + \"\\n\"\n )\n review_path = tmp_path / \"review.txt\"\n review_path.write_text(\"### Codex independent review\\n\\nNo issues found.\\n\")\n\n exit_code = analyze_mod.main(\n [\"analyze-latest-review.py\", str(archive_path), str(review_path)]\n )\n assert exit_code == 0\n out = capsys.readouterr().out\n assert '\"newly_crossed\": []' in out\n\n\ndef test_main_cli_reports_newly_crossed_topic(tmp_path, capsys):\n archive_path = tmp_path / \"archive.jsonl\"\n lines = [\n json.dumps(_entry(1, [\"**[P1]** Secret token leaked in stdout.\"])),\n json.dumps(_entry(2, [\"**[P2]** Credential redaction missed a field.\"])),\n ]\n archive_path.write_text(\"\\n\".join(lines) + \"\\n\")\n\n review_path = tmp_path / \"review.txt\"\n review_path.write_text(\n \"1. **[P1]** Another secret exposed in stderr on failure.\\n\"\n )\n\n exit_code = analyze_mod.main(\n [\"analyze-latest-review.py\", str(archive_path), str(review_path)]\n )\n assert exit_code == 0\n out = capsys.readouterr().out\n payload = json.loads(out.split(\"---\\n\", 1)[1])\n topics = {c[\"topic\"] for c in payload[\"newly_crossed\"]}\n assert \"credential-redaction\" in topics\n\"\"\"Tests for detect-recurring-pattern.py.\n\nRun with: python3 -m pytest scripts/detect_recurring_pattern_test.py -q\n\"\"\"\n\nimport importlib.util\nimport json\nimport sys\nfrom pathlib import Path\n\n_MODULE_PATH = Path(__file__).parent / \"detect-recurring-pattern.py\"\n_spec = importlib.util.spec_from_file_location(\"detect_recurring_pattern\", _MODULE_PATH)\nassert _spec is not None and _spec.loader is not None\ndetect = importlib.util.module_from_spec(_spec)\nsys.modules[\"detect_recurring_pattern\"] = detect\n_spec.loader.exec_module(detect)\n\n\ndef test_classify_finding_matches_known_topics():\n assert detect.classify_finding(\"[P1] Redact the leaked credential\") == \"credential-redaction\"\n assert detect.classify_finding(\"bash -e aborts before exit code capture\") == \"shell-semantics\"\n # Deliberately avoids the word \"token\": credential-redaction's keyword\n # list includes \"token\" too, and is checked first, so any example\n # mentioning GITHUB_TOKEN would match there instead — reasonably, since\n # GITHUB_TOKEN genuinely is credential-adjacent. This example isolates\n # fork-pr-permissions specifically.\n assert (\n detect.classify_finding(\"Fork-originated pull requests cannot receive posted comments\")\n == \"fork-pr-permissions\"\n )\n # Same reasoning: avoids \"credential\" (which would match\n # credential-redaction first) to isolate auth-lifecycle specifically.\n assert (\n detect.classify_finding(\"auth.json rotates and the old value goes stale after expiring\")\n == \"auth-lifecycle\"\n )\n\n\ndef test_credential_redaction_keyword_wins_over_other_topics_when_both_present():\n \"\"\"Documents the real, reasonable behavior the fixed test above works\n around: a finding mentioning GITHUB_TOKEN is credential-adjacent, so it\n is classified as credential-redaction even when it's really about fork\n permissions specifically. Topic buckets are approximate by design (see\n module docstring) — this pins the actual priority order rather than\n leaving it as an implicit, undocumented side effect.\"\"\"\n assert (\n detect.classify_finding(\"Fork PRs fail: GITHUB_TOKEN read-only\")\n == \"credential-redaction\"\n )\n\n\ndef test_classify_finding_returns_none_for_unmatched_text():\n assert detect.classify_finding(\"this finding matches no known topic at all\") is None\n\n\ndef test_recommends_mechanism_fix_once_topic_recurs_at_threshold():\n entries = [\n {\"round\": 1, \"findings\": [\"[P1] leaked credential in output\"]},\n {\"round\": 2, \"findings\": [\"[P1] secret token exposed again\"]},\n {\"round\": 3, \"findings\": [\"[P2] another credential redaction gap\"]},\n ]\n result = detect.analyze(entries, threshold=3)\n rec = next(r for r in result[\"recommendations\"] if r[\"topic\"] == \"credential-redaction\")\n assert rec[\"recurrence_count\"] == 3\n assert rec[\"recommended_action\"] == \"mechanism\"\n assert rec[\"rounds\"] == [1, 2, 3]\n\n\ndef test_recommends_target_fix_below_threshold():\n entries = [\n {\"round\": 1, \"findings\": [\"[P1] leaked credential in output\"]},\n {\"round\": 2, \"findings\": [\"[P1] secret token exposed again\"]},\n ]\n result = detect.analyze(entries, threshold=3)\n rec = next(r for r in result[\"recommendations\"] if r[\"topic\"] == \"credential-redaction\")\n assert rec[\"recurrence_count\"] == 2\n assert rec[\"recommended_action\"] == \"target\"\n\n\ndef test_same_round_multiple_findings_same_topic_counts_once():\n \"\"\"Recurrence is measured in distinct ROUNDS a topic appears in, not raw\n finding count -- five credential findings in one round is one round of\n evidence, not five, otherwise a single verbose round could trip the\n threshold on its own.\"\"\"\n entries = [\n {\n \"round\": 1,\n \"findings\": [\n \"[P1] credential leak A\",\n \"[P1] credential leak B\",\n \"[P2] credential leak C\",\n ],\n },\n {\"round\": 2, \"findings\": [\"[P1] credential leak D\"]},\n ]\n result = detect.analyze(entries, threshold=3)\n rec = next(r for r in result[\"recommendations\"] if r[\"topic\"] == \"credential-redaction\")\n assert rec[\"recurrence_count\"] == 2\n assert rec[\"recommended_action\"] == \"target\"\n\n\ndef test_real_archive_recommends_mechanism_fix_for_credential_redaction(tmp_path):\n \"\"\"Regression proof against this repo's own real archive data: this must\n reproduce the same 'revise the mechanism, not just the target' call that\n was made manually before round 5 -- derived from evidence, not asserted.\"\"\"\n archive_path = Path(__file__).parent.parent / \"docs\" / \"self-improvement-archive.jsonl\"\n entries = []\n with open(archive_path) as f:\n for line in f:\n line = line.strip()\n if line:\n entries.append(json.loads(line))\n\n result = detect.analyze(entries, threshold=3)\n rec = next(\n (r for r in result[\"recommendations\"] if r[\"topic\"] == \"credential-redaction\"), None\n )\n assert rec is not None, \"expected credential-redaction topic to appear in the real archive\"\n assert rec[\"recommended_action\"] == \"mechanism\"\n assert rec[\"recurrence_count\"] >= 3\n\n\ndef test_main_cli_runs_against_a_file_and_exits_zero(tmp_path):\n archive = tmp_path / \"archive.jsonl\"\n archive.write_text(\n json.dumps({\"round\": 1, \"findings\": [\"[P1] credential leak\"]})\n + \"\\n\"\n + json.dumps({\"round\": 2, \"findings\": [\"[P1] credential leak again\"]})\n + \"\\n\"\n )\n exit_code = detect.main([\"detect-recurring-pattern.py\", str(archive), \"--threshold\", \"2\"])\n assert exit_code == 0\n{\"round\": 1, \"target\": \".github/workflows/codex-review.yml\", \"proposal\": \"Wire Codex as an automated PR reviewer (backlog item #4): run codex exec against every PR diff, post findings as a comment, fail the check on a [P1].\", \"proposed_by\": \"claude-sonnet-5\", \"validator\": \"codex (gpt-6-astra, real live execution via user's ChatGPT subscription)\", \"validation_method\": \"actual CI run on PR #2 (github.com/gagan114662/open-inspect-72e95a/pull/2) using the real CODEX_AUTH_JSON secret\", \"result\": \"rejected_with_findings\", \"findings\": [\"[P1] Review credentials are readable by the agent reviewing untrusted code — same-repo PR could prompt-inject exfiltration of auth.json into the posted comment.\", \"[P2] Failed reviews (crash/timeout/auth error) were swallowed and reported as a passing check.\", \"[P2] Prompt passed as a single shell argument; large diffs would hit 'Argument list too long'.\", \"[P2] Comment-posting step ran unconditionally, would fail on fork PRs whose default GITHUB_TOKEN is read-only.\", \"[P2] [P1] detection used a bare substring grep that 'No [P1] findings' would itself trigger.\"], \"commit\": null, \"kept\": false, \"occurred_at\": \"2026-09-14T15:50:28Z\"}\n{\"round\": 2, \"target\": \".github/workflows/codex-review.yml\", \"proposal\": \"Fix all 5 round-1 findings: redact the literal secret from output, fail closed on a crashed/timed-out review, pipe the prompt via stdin instead of a shell argument, skip comment-posting on fork PRs, anchor [P1] detection to the required **[P1]** format.\", \"proposed_by\": \"claude-sonnet-5\", \"validator_local\": \"mocked codex/git/timeout binaries, run against the real extracted run: script (parsed from the actual YAML, not hand-copied) for both the success and induced-failure paths\", \"validator_ci\": \"codex (gpt-6-astra, real live execution)\", \"validation_method\": \"local mock execution before push, then actual CI run on PR #2 with the real secret\", \"result\": \"rejected_with_findings\", \"findings\": [\"[P1] Redaction concatenated all credential values into one string before matching; an individual JSON field value (e.g. a bare access_token) echoed on its own would survive.\", \"[P1] actions/checkout persists the job's own GITHUB_TOKEN in git config by default, readable by the same agent — a second exfiltration path not covered by the round-1 fix.\", \"[P2] CODEX_API_KEY/OPENAI_API_KEY were exported unconditionally; Codex gives an API key precedence over stored ChatGPT auth when both are present, which could silently defeat subscription mode.\"], \"commit\": \"70c6c5ec\", \"kept\": true, \"kept_reason\": \"All 5 round-1 findings genuinely fixed and locally verified (the fix itself was correct and kept); 3 new findings were found on the *next* layer of the same file, which is round 3's proposal, not a rejection of round 2's actual changes.\", \"occurred_at\": \"2026-09-14T15:57:15Z\"}\n{\"round\": 3, \"target\": \".github/workflows/codex-review.yml\", \"proposal\": \"Fix all 3 round-2 findings: redact each individual JSON field value rather than the whole concatenated blob, set persist-credentials: false on checkout, only export the selected auth mode's env vars.\", \"proposed_by\": \"claude-sonnet-5\", \"validator_local\": \"mocked codex/git/timeout binaries, run against the real extracted run: script, with a mock auth.json containing multiple distinct token-shaped fields to prove per-field redaction\", \"validator_ci\": \"codex (gpt-6-astra, real live execution)\", \"validation_method\": \"local mock execution before push, then actual CI run on PR #2 with the real secret\", \"result\": \"rejected_with_findings\", \"findings\": [\"[P1] The 'Check for Codex credentials' step interpolates ${{ secrets.X }} directly into its generated shell script body (not via env:), so ALL configured credentials — including ones excluded from the selected mode — get baked into that step's own temp script file on disk, readable independent of the later redaction logic, which only knows about the selected mode's values.\", \"[P2] auth.json is rewritten fresh from the static secret on every run; if Codex rotates its refresh token mid-run, the next run reuses the stale value with no write-back, eventually causing auth failures.\", \"[P2] GitHub Actions invokes run: steps with `bash -e` by default (errexit); `set -uo pipefail` does not disable inherited -e, so a nonzero codex exec exit aborts the script immediately, before the exit_code=$? capture line ever runs — the round-2 'fail closed' fix does not actually execute as designed. This was invisible to local testing because the local mock harness ran plain `bash script.sh`, not `bash -e script.sh` — a real blind spot in the validation method itself, not just the code.\"], \"commit\": null, \"kept\": false, \"occurred_at\": \"2026-09-14T16:02:13Z\", \"note\": \"Round 3's third finding is itself a finding about the *validator* (local mock testing didn't replicate GitHub's actual shell invocation flags) — fixed for round 4 by testing with `bash -e` explicitly, not just plain bash.\"}\n{\"round\": 4, \"target\": \".github/workflows/codex-review.yml\", \"proposal\": \"Fix round-3's 3 findings: move secret checks to env: boolean flags (no raw values interpolated into script text), fix the errexit blind spot by using the codex exec call as an if-condition (bash exempts if-conditions from -e), add an actionable warning on auth-looking failures.\", \"proposed_by\": \"claude-sonnet-5\", \"validator_local\": \"mocked codex/git/timeout binaries, run against the real extracted run: script under bash -e explicitly this time (not plain bash) for both the induced-failure and success paths, specifically to close the exact blind spot round 3 identified in the validation method itself\", \"validator_ci\": \"codex (gpt-6-astra, real live execution)\", \"validation_method\": \"local mock execution under bash -e before push, then actual CI run on PR #2 with the real secret\", \"result\": \"rejected_with_findings\", \"findings\": [\"[P1] The failure-path `cat /tmp/codex-review-err.txt` prints Codex's raw stderr trace directly to job logs, bypassing the redaction logic entirely — that logic only runs on the success path. If Codex reads credential material before crashing/timing out, an individual token value could leak into logs unredacted.\", \"[P2] Configuring only OPENAI_API_KEY (not CODEX_API_KEY) …9981 tokens truncated…ked`\nL195: - `category_changed`\nL196: - `answered`\nL197: - `unanswered` | Last commit on default branch | Default branch\nL198: \nL199: Note\nL200: * More than one activity type triggers this event. For information about each activity type, see cite67†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL201: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL202: * Webhook events for GitHub Discussions are currently in public preview and subject to change.\nL203: Runs your workflow when a discussion in the workflow's repository is created or modified. For activity related to comments on a discussion, use the cite14†`discussion_comment` event. For more information about discussions, see cite68†About discussions . For information about the GraphQL API, see cite69†Discussions .\nL204: \nL205: For example, you can run a workflow when a discussion has been `created`, `edited`, or `answered`.\nL206: \nL207: `on:\nL208: discussion:\nL209: types: [created, edited, answered]\nL210: `\nL211: ## cite14†`discussion_comment` L212: \nL213: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL214: --- | --- | --- | ---\nL215: cite70†`discussion_comment` | - `created`\nL216: - `edited`\nL217: - `deleted`\nL218: | Last commit on default branch | Default branch\nL219: \nL220: Note\nL221: * More than one activity type triggers this event. For information about each activity type, see cite70†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL222: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL223: * Webhook events for GitHub Discussions are currently in public preview and subject to change.\nL224: Runs your workflow when a comment on a discussion in the workflow's repository is created or modified. For activity related to a discussion as opposed to comments on the discussion, use the cite13†`discussion` event. For more information about discussions, see cite68†About discussions . For information about the GraphQL API, see cite69†Discussions .\nL225: For example, you can run a workflow when a discussion comment has been `created` or `deleted`.\nL226: \nL227: `on:\nL228: discussion_comment:\nL229: types: [created, deleted]\nL230: `\nL231: ## cite15†`fork` L232: \nL233: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL234: --- | --- | --- | ---\nL235: cite71†`fork` | Not applicable | Last commit on default branch | Default branch\nL236: \nL237: Note\nL238: \nL239: This event will only trigger a workflow run if the workflow file exists on the default branch.\nL240: \nL241: Runs your workflow when someone forks a repository. For information about the REST API, see cite72†REST API endpoints for forks .\nL242: For example, you can run a workflow when the `fork` event occurs.\nL243: \nL244: `on:\nL245: fork\nL246: `\nL247: ## cite16†`gollum` L248: \nL249: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL250: --- | --- | --- | ---\nL251: cite73†`gollum` | Not applicable | Last commit on default branch | Default branch\nL252: \nL253: Note\nL254: \nL255: This event will only trigger a workflow run if the workflow file exists on the default branch.\nL256: \nL257: Runs your workflow when someone creates or updates a Wiki page. For more information, see cite74†About wikis .\nL258: \nL259: For example, you can run a workflow when the `gollum` event occurs.\nL260: \nL261: `on:\nL262: gollum\nL263: `\nL264: ## cite17†`image_version` L265: \nL266: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL267: --- | --- | --- | ---\nL268: Not applicable | Not applicable | Last commit on default branch | Default branch\nL269: \nL270: Runs your workflow when a new version of a specified image becomes available for use. This event is typically triggered after a successful image version creation, allowing you to automate actions such as deployment or notifications in response to new image versions.\nL271: This event supports glob patterns for both image names and versions. The example below triggers when a new image version matches any of the specified name and version combinations. For example, `[\"MyNewImage\", 1.0.0]`, `[\"MyNewImage\", 2.53.0]`, `[\"MyOtherImage\", 1.0.0]`, and `[\"MyOtherImage\", 2.0.0]`.\nL272: \nL273: `on:\nL274: image_version:\nL275: names:\nL276: - \"MyNewImage\"\nL277: - \"MyOtherImage\"\nL278: versions:\nL279: - 1.*\nL280: - 2.*\nL281: `\nL282: ## cite18†`issue_comment` L283: \nL284: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL285: --- | --- | --- | ---\nL286: cite75†`issue_comment` | - `created`\nL287: - `edited`\nL288: - `deleted`\nL289: | Last commit on default branch | Default branch\nL290: \nL291: Note\nL292: * More than one activity type triggers this event. For information about each activity type, see cite75†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL293: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL294: Runs your workflow when an issue or pull request comment is created, edited, or deleted. For information about the issue comment APIs, see cite76†Issues in the GraphQL API documentation or cite75†Webhook events and payloads in the REST API documentation.\nL295: \nL296: For example, you can run a workflow when an issue or pull request comment has been `created` or `deleted`.\nL297: \nL298: `on:\nL299: issue_comment:\nL300: types: [created, deleted]\nL301: `\nL302: ### cite77†`issue_comment` on issues only or pull requests only L303: \nL304: The `issue_comment` event occurs for comments on both issues and pull requests. You can use the `github.event.issue.pull_request` property in a conditional to take different action depending on whether the triggering object was an issue or pull request.\nL305: For example, this workflow will run the `pr_commented` job only if the `issue_comment` event originated from a pull request. It will run the `issue_commented` job only if the `issue_comment` event originated from an issue.\nL306: \nL307: `on: issue_comment\nL308: \nL309: jobs:\nL310: pr_commented:\nL311: # This job only runs for pull request comments\nL312: name: PR comment\nL313: if: ${{ github.event.issue.pull_request }}\nL314: runs-on: ubuntu-latest\nL315: steps:\nL316: - run: |\nL317: echo A comment on PR $NUMBER\nL318: env:\nL319: NUMBER: ${{ github.event.issue.number }}\nL320: \nL321: issue_commented:\nL322: # This job only runs for issue comments\nL323: name: Issue comment\nL324: if: ${{ !github.event.issue.pull_request }}\nL325: runs-on: ubuntu-latest\nL326: steps:\nL327: - run: |\nL328: echo A comment on issue $NUMBER\nL329: env:\nL330: NUMBER: ${{ github.event.issue.number }}\nL331: `\nL332: ## cite19†`issues` L333: \nL334: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL335: --- | --- | --- | ---\nL336: cite78†`issues` | - `opened`\nL337: - `edited`\nL338: - `deleted`\nL339: - `transferred`\nL340: - `pinned`\nL341: - `unpinned`\nL342: - `closed`\nL343: - `reopened`\nL344: - `assigned`\nL345: - `unassigned`\nL346: - `labeled`\nL347: - `unlabeled`\nL348: - `locked`\nL349: - `unlocked`\nL350: - `milestoned`\nL351: - `demilestoned`\nL352: - `typed`\nL353: - `untyped`\nL354: - `field_added`\nL355: - `field_removed` | Last commit on default branch | Default branch\nL356: \nL357: Note\nL358: * More than one activity type triggers this event. For information about each activity type, see cite78†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL359: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL360: Runs your workflow when an issue in the workflow's repository is created or modified. For activity related to comments in an issue, use the cite18†`issue_comment` event. For more information about issues, see cite79†About issues . For information about the issue APIs, see cite80†Issues in the GraphQL API documentation or cite81†REST API endpoints for issues .\nL361: For example, you can run a workflow when an issue has been `opened`, `edited`, or `milestoned`.\nL362: \nL363: `on:\nL364: issues:\nL365: types: [opened, edited, milestoned]\nL366: `\nL367: You can also run a workflow when an issue field value is set, changed, or cleared. The `field_added` activity type fires both when a field value is initially set and when an existing value is updated. The `field_removed` activity type fires when a field value is cleared.\nL368: \nL369: `on:\nL370: issues:\nL371: types: [field_added, field_removed]\nL372: `\nL373: ## cite20†`label` L374: \nL375: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL376: --- | --- | --- | ---\nL377: cite82†`label` | - `created`\nL378: - `edited`\nL379: - `deleted`\nL380: | Last commit on default branch | Default branch\nL381: \nL382: Note\nL383: * More than one activity type triggers this event. For information about each activity type, see cite82†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL384: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL385: Runs your workflow when a label in your workflow's repository is created or modified. For more information about labels, see cite83†Managing labels . For information about the label APIs, see cite84†Issues in the GraphQL API documentation or cite85†REST API endpoints for labels .\nL386: If you want to run your workflow when a label is added to or removed from an issue, pull request, or discussion, use the `labeled` or `unlabeled` activity types for the cite19†`issues` , cite25†`pull_request` , cite29†`pull_request_target` , or cite13†`discussion` events instead.\nL387: \nL388: For example, you can run a workflow when a label has been `created` or `deleted`.\nL389: \nL390: `on:\nL391: label:\nL392: types: [created, deleted]\nL393: `\nL394: ## cite21†`merge_group` L395: \nL396: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL397: --- | --- | --- | ---\nL398: cite86†`merge_group` | `checks_requested` | SHA of the merge group | Ref of the merge group\nL399: \nL400: Note\nL401: * More than one activity type triggers this event. Although only the `checks_requested` activity type is supported, specifying the activity type will keep your workflow specific if more activity types are added in the future. For information about each activity type, see cite86†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword.\nL402: For more information, see cite44†Workflow syntax for GitHub Actions .\nL403: * If your repository uses GitHub Actions to perform required checks on pull requests in your repository, you need to update the workflows to include the `merge_group` event as an additional trigger. Otherwise, status checks will not be triggered when you add a pull request to a merge queue. The merge will fail as the required status check will not be reported. The `merge_group` event is separate from the `pull_request` and `push` events.\nL404: Runs your workflow when a pull request is added to a merge queue, which adds the pull request to a merge group. For more information see cite87†Merging a pull request with a merge queue .\nL405: \nL406: For example, you can run a workflow when the `checks_requested` activity has occurred.\nL407: \nL408: `on:\nL409: pull_request:\nL410: branches: [ \"main\" ]\nL411: merge_group:\nL412: types: [checks_requested]\nL413: `\nL414: ## cite22†`milestone` L415: \nL416: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL417: --- | --- | --- | ---\nL418: cite88†`milestone` | - `created`\nL419: - `closed`\nL420: - `opened`\nL421: - `edited`\nL422: - `deleted`\nL423: | Last commit on default branch | Default branch\nL424: \nL425: Note\nL426: * More than one activity type triggers this event. For information about each activity type, see cite88†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL427: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL428: Runs your workflow when a milestone in the workflow's repository is created or modified. For more information about milestones, see cite89†About milestones . For information about the milestone APIs, see cite90†Issues in the GraphQL API documentation or cite91†REST API endpoints for milestones .\nL429: \nL430: If you want to run your workflow when an issue is added to or removed from a milestone, use the `milestoned` or `demilestoned` activity types for the cite19†`issues` event instead.\nL431: For example, you can run a workflow when a milestone has been `opened` or `deleted`.\nL432: \nL433: `on:\nL434: milestone:\nL435: types: [opened, deleted]\nL436: `\nL437: ## cite23†`page_build` L438: \nL439: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL440: --- | --- | --- | ---\nL441: cite92†`page_build` | Not applicable | Last commit on default branch | Default branch\nL442: \nL443: Note\nL444: \nL445: This event will only trigger a workflow run if the workflow file exists on the default branch.\nL446: Runs your workflow when someone pushes to a branch that is the publishing source for GitHub Pages, if GitHub Pages is enabled for the repository. For more information about GitHub Pages publishing sources, see cite93†Configuring a publishing source for your GitHub Pages site . For information about the REST API, see cite94†REST API endpoints for repositories .\nL447: \nL448: For example, you can run a workflow when the `page_build` event occurs.\nL449: \nL450: `on:\nL451: page_build\nL452: `\nL453: ## cite24†`public` L454: \nL455: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL456: --- | --- | --- | ---\nL457: cite95†`public` | Not applicable | Last commit on default branch | Default branch\nL458: \nL459: Note\nL460: \nL461: This event will only trigger a workflow run if the workflow file exists on the default branch.\nL462: \nL463: Runs your workflow when your workflow's repository changes from private to public. For information about the REST API, see cite96†REST API endpoints for repositories .\nL464: For example, you can run a workflow when the `public` event occurs.\nL465: \nL466: `on:\nL467: public\nL468: `\nL469: ## cite25†`pull_request` L470: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL471: --- | --- | --- | ---\nL472: cite97†`pull_request` | - `assigned`\nL473: - `unassigned`\nL474: - `labeled`\nL475: - `unlabeled`\nL476: - `opened`\nL477: - `edited`\nL478: - `closed`\nL479: - `reopened`\nL480: - `synchronize`\nL481: - `converted_to_draft`\nL482: - `locked`\nL483: - `unlocked`\nL484: - `enqueued`\nL485: - `dequeued`\nL486: - `milestoned`\nL487: - `demilestoned`\nL488: - `ready_for_review`\nL489: - `review_requested`\nL490: - `review_request_removed`\nL491: - `auto_merge_enabled`\nL492: - `auto_merge_disabled` | Last merge commit on the `GITHUB_REF` branch | PR merge branch `refs/pull/PULL_REQUEST_NUMBER/merge`\nL493: Note\nL494: * More than one activity type triggers this event. For information about each activity type, see cite97†Webhook events and payloads . By default, a workflow only runs when a `pull_request` event's activity type is `opened`, `synchronize`, or `reopened`. To trigger workflows by different activity types, use the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL495: * Workflows will not run on `pull_request` activity if the pull request has a merge conflict. The merge conflict must be resolved first. Conversely, workflows with the `pull_request_target` event will run even if the pull request has a merge conflict. Before using the `pull_request_target` trigger, you should be aware of the security risks. For more information, see cite29†`pull_request_target` .\nL496: * The `pull_request` webhook event payload is empty for merged pull requests and pull requests that come from forked repositories.\nL497: * When a pull request is created or updated by a workflow using `GITHUB_TOKEN`, `pull_request` events with the `opened`, `synchronize`, or `reopened` activity types create workflow runs that require approval. A user with write access to the repository can approve these runs from the pull request page. With the exception of `workflow_dispatch` and `repository_dispatch`, other `GITHUB_TOKEN`-triggered events do not create workflow runs at all.\nL498: * The value of `GITHUB_REF` varies for a closed pull request depending on whether the pull request has been merged or not. If a pull request was closed but not merged, it will be `refs/pull/PULL_REQUEST_NUMBER/merge`. If a pull request was closed as a result of being merged, it will be the fully qualified `ref` of the branch it was merged into, for example `/refs/heads/main`.\n--------------------------------------------------------------------------------\ncheckout/src/input-helper.ts at v4 · actions/checkout · GitHub (https://github.com/actions/checkout/blob/v4/src/input-helper.ts)\nciteturn0view1 [wordlim: 200] Crawled: today; Content type: text/html; Source: open({\"ref_id\":\"https://github.com/actions/checkout/blob/v4/src/input-helper.ts\",\"lineno\":null}); Total lines: 939\n"}}>cat scripts/parse-review-findings.py scripts/analyze_latest_review_test.py scripts/detect_recurring_pattern_test.py docs/self-improvement-archive.jsonl .github/workflows/codex-review.yml{"i":0,"result":{"status":"fulfilled","value":{"chunk_id":"b54d27","wall_time_seconds":6.25e-7,"exit_code":0,"original_token_count":276,"output":"# 网页阅读\n\n通用网页、RSS。\n\n## 通用网页 (Jina Reader)\n\n```bash\n# 读取任意网页内容\ncurl -s \"https://r.jina.ai/URL\"\n\n# 示例\ncurl -s \"https://r.jina.ai/https://example.com/article\"\n```\n\n**适用场景**: 大多数网页可以直接用 Jina Reader 读取。\n\n## Web Reader (MCP)\n\n```bash\n# 读取网页内容 (Markdown 格式)\nmcporter call 'web-reader.webReader(url: \"https://example.com\")'\n\n# 保留图片\nmcporter call 'web-reader.webReader(url: \"https://example.com\", retain_images: true)'\n\n# 纯文本格式\nmcporter call 'web-reader.webReader(url: \"https://example.com\", return_format: \"text\")'\n```\n\n**适用场景**: 需要更精确控制输出格式时使用。\n\n## RSS (feedparser)\n\n```python\npython3 -c \"\nimport feedparser\nfor e in feedparser.parse('FEED_URL').entries[:5]:\n print(f'{e.title} — {e.link}')\n\"\n```\n\n**适用场景**: 订阅博客、新闻源、播客等 RSS feed。\n\n## 选择指南\n\n| 场景 | 推荐工具 |\n|-----|---------|\n| 通用网页 | Jina Reader (`curl r.jina.ai`) |\n| 需要图片/格式控制 | web-reader MCP |\n| RSS 订阅 | feedparser |\n"}}}
{"i":1,"result":{"status":"fulfilled","value":{"chunk_id":"8e852e","wall_time_seconds":0.00000275,"exit_code":0,"original_token_count":11279,"output":"#!/usr/bin/env python3\n\"\"\"Parse [P1]/[P2] findings out of a Codex review comment's raw text.\n\nExtracted as its own tested tool because the next piece of this archive\n(the workflow that reads a completed Codex review and decides, on its own,\nwhether the recurrence pattern warrants opening a tracking issue) needs a\nreliable, reusable way to turn free-form review prose back into a list of\nindividual findings -- the same shape docs/self-improvement-archive.jsonl\nalready stores per round.\n\nFindings in this archive's own convention start a line with a number, a\nperiod, and a **[P1]** or **[P2]** marker, e.g.:\n 1. **[P1]** Some critical issue description.\nA finding may span multiple lines until the next numbered marker or the end\nof the text; this parser keeps only the first line to match how findings\nare already recorded in the archive (short, single-line summaries).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nimport sys\n\nFINDING_LINE_RE = re.compile(r\"^\\s*\\d+\\.\\s*(\\*\\*\\[(P1|P2)\\]\\*\\*.*)$\")\n\n\ndef parse_findings(text: str) -> list[str]:\n findings = []\n for line in text.splitlines():\n match = FINDING_LINE_RE.match(line)\n if match:\n findings.append(match.group(1).strip())\n return findings\n\n\ndef main(argv: list[str]) -> int:\n if len(argv) == 2:\n with open(argv[1]) as f:\n text = f.read()\n elif len(argv) == 1:\n text = sys.stdin.read()\n else:\n print(\"usage: parse-review-findings.py [file] (reads stdin if omitted)\", file=sys.stderr)\n return 2\n\n findings = parse_findings(text)\n print(json.dumps(findings, indent=2))\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main(sys.argv))\n\"\"\"Tests for analyze-latest-review.py.\n\nRun with: python3 -m pytest scripts/analyze_latest_review_test.py -q\n\"\"\"\n\nimport importlib.util\nimport json\nimport sys\nfrom pathlib import Path\n\n_MODULE_PATH = Path(__file__).parent / \"analyze-latest-review.py\"\n_spec = importlib.util.spec_from_file_location(\"analyze_latest_review\", _MODULE_PATH)\nassert _spec is not None and _spec.loader is not None\nanalyze_mod = importlib.util.module_from_spec(_spec)\nsys.modules[\"analyze_latest_review\"] = analyze_mod\n_spec.loader.exec_module(analyze_mod)\n\n\ndef _entry(round_num, findings):\n return {\"round\": round_num, \"findings\": findings}\n\n\ndef test_topic_not_yet_crossed_and_new_round_pushes_it_over():\n \"\"\"Two prior rounds mention credential redaction (below threshold 3);\n a new round's findings supply the third -> must be reported as newly\n crossed.\"\"\"\n archive = [\n _entry(1, [\"**[P1]** Secret token leaked in stdout.\"]),\n _entry(2, [\"**[P2]** Credential redaction missed a field.\"]),\n ]\n new_findings = [\"**[P1]** Another secret exposed in stderr.\"]\n\n crossed = analyze_mod.find_newly_crossed_topics(archive, new_findings, threshold=3)\n topics = {c[\"topic\"] for c in crossed}\n assert \"credential-redaction\" in topics\n\n\ndef test_topic_already_at_mechanism_level_is_not_reported_again():\n \"\"\"A topic that already recommended 'mechanism' in the archive alone\n must NOT be reported every subsequent round -- only the round that\n first tips it over counts as 'newly crossed'.\"\"\"\n archive = [\n _entry(1, [\"**[P1]** Secret leaked.\"]),\n _entry(2, [\"**[P2]** Credential redaction missed a field.\"]),\n _entry(3, [\"**[P1]** Token exposed again.\"]),\n ]\n # Already at/above threshold 3 without the new round.\n new_findings = [\"**[P2]** Yet another credential leak.\"]\n\n crossed = analyze_mod.find_newly_crossed_topics(archive, new_findings, threshold=3)\n assert crossed == []\n\n\ndef test_unrelated_new_finding_does_not_falsely_cross():\n archive = [\n _entry(1, [\"**[P1]** Secret leaked.\"]),\n _entry(2, [\"**[P2]** Credential redaction missed a field.\"]),\n ]\n new_findings = [\"**[P2]** Minor typo in a comment.\"]\n\n crossed = analyze_mod.find_newly_crossed_topics(archive, new_findings, threshold=3)\n assert crossed == []\n\n\ndef test_empty_archive_with_new_round_below_threshold_reports_nothing():\n crossed = analyze_mod.find_newly_crossed_topics([], [\"**[P1]** Secret leaked once.\"], threshold=3)\n assert crossed == []\n\n\ndef test_next_round_number_from_empty_archive_is_one():\n assert analyze_mod.next_round_number([]) == 1\n\n\ndef test_next_round_number_increments_from_max():\n archive = [_entry(1, []), _entry(4, []), _entry(2, [])]\n assert analyze_mod.next_round_number(archive) == 5\n\n\ndef test_main_cli_with_real_archive_and_no_findings_in_review(tmp_path, capsys):\n archive_path = tmp_path / \"archive.jsonl\"\n archive_path.write_text(\n json.dumps(_entry(1, [\"**[P1]** Secret leaked.\"])) + \"\\n\"\n )\n review_path = tmp_path / \"review.txt\"\n review_path.write_text(\"### Codex independent review\\n\\nNo issues found.\\n\")\n\n exit_code = analyze_mod.main(\n [\"analyze-latest-review.py\", str(archive_path), str(review_path)]\n )\n assert exit_code == 0\n out = capsys.readouterr().out\n assert '\"newly_crossed\": []' in out\n\n\ndef test_main_cli_reports_newly_crossed_topic(tmp_path, capsys):\n archive_path = tmp_path / \"archive.jsonl\"\n lines = [\n json.dumps(_entry(1, [\"**[P1]** Secret token leaked in stdout.\"])),\n json.dumps(_entry(2, [\"**[P2]** Credential redaction missed a field.\"])),\n ]\n archive_path.write_text(\"\\n\".join(lines) + \"\\n\")\n\n review_path = tmp_path / \"review.txt\"\n review_path.write_text(\n \"1. **[P1]** Another secret exposed in stderr on failure.\\n\"\n )\n\n exit_code = analyze_mod.main(\n [\"analyze-latest-review.py\", str(archive_path), str(review_path)]\n )\n assert exit_code == 0\n out = capsys.readouterr().out\n payload = json.loads(out.split(\"---\\n\", 1)[1])\n topics = {c[\"topic\"] for c in payload[\"newly_crossed\"]}\n assert \"credential-redaction\" in topics\n\"\"\"Tests for detect-recurring-pattern.py.\n\nRun with: python3 -m pytest scripts/detect_recurring_pattern_test.py -q\n\"\"\"\n\nimport importlib.util\nimport json\nimport sys\nfrom pathlib import Path\n\n_MODULE_PATH = Path(__file__).parent / \"detect-recurring-pattern.py\"\n_spec = importlib.util.spec_from_file_location(\"detect_recurring_pattern\", _MODULE_PATH)\nassert _spec is not None and _spec.loader is not None\ndetect = importlib.util.module_from_spec(_spec)\nsys.modules[\"detect_recurring_pattern\"] = detect\n_spec.loader.exec_module(detect)\n\n\ndef test_classify_finding_matches_known_topics():\n assert detect.classify_finding(\"[P1] Redact the leaked credential\") == \"credential-redaction\"\n assert detect.classify_finding(\"bash -e aborts before exit code capture\") == \"shell-semantics\"\n # Deliberately avoids the word \"token\": credential-redaction's keyword\n # list includes \"token\" too, and is checked first, so any example\n # mentioning GITHUB_TOKEN would match there instead — reasonably, since\n # GITHUB_TOKEN genuinely is credential-adjacent. This example isolates\n # fork-pr-permissions specifically.\n assert (\n detect.classify_finding(\"Fork-originated pull requests cannot receive posted comments\")\n == \"fork-pr-permissions\"\n )\n # Same reasoning: avoids \"credential\" (which would match\n # credential-redaction first) to isolate auth-lifecycle specifically.\n assert (\n detect.classify_finding(\"auth.json rotates and the old value goes stale after expiring\")\n == \"auth-lifecycle\"\n )\n\n\ndef test_credential_redaction_keyword_wins_over_other_topics_when_both_present():\n \"\"\"Documents the real, reasonable behavior the fixed test above works\n around: a finding mentioning GITHUB_TOKEN is credential-adjacent, so it\n is classified as credential-redaction even when it's really about fork\n permissions specifically. Topic buckets are approximate by design (see\n module docstring) — this pins the actual priority order rather than\n leaving it as an implicit, undocumented side effect.\"\"\"\n assert (\n detect.classify_finding(\"Fork PRs fail: GITHUB_TOKEN read-only\")\n == \"credential-redaction\"\n )\n\n\ndef test_classify_finding_returns_none_for_unmatched_text():\n assert detect.classify_finding(\"this finding matches no known topic at all\") is None\n\n\ndef test_recommends_mechanism_fix_once_topic_recurs_at_threshold():\n entries = [\n {\"round\": 1, \"findings\": [\"[P1] leaked credential in output\"]},\n {\"round\": 2, \"findings\": [\"[P1] secret token exposed again\"]},\n {\"round\": 3, \"findings\": [\"[P2] another credential redaction gap\"]},\n ]\n result = detect.analyze(entries, threshold=3)\n rec = next(r for r in result[\"recommendations\"] if r[\"topic\"] == \"credential-redaction\")\n assert rec[\"recurrence_count\"] == 3\n assert rec[\"recommended_action\"] == \"mechanism\"\n assert rec[\"rounds\"] == [1, 2, 3]\n\n\ndef test_recommends_target_fix_below_threshold():\n entries = [\n {\"round\": 1, \"findings\": [\"[P1] leaked credential in output\"]},\n {\"round\": 2, \"findings\": [\"[P1] secret token exposed again\"]},\n ]\n result = detect.analyze(entries, threshold=3)\n rec = next(r for r in result[\"recommendations\"] if r[\"topic\"] == \"credential-redaction\")\n assert rec[\"recurrence_count\"] == 2\n assert rec[\"recommended_action\"] == \"target\"\n\n\ndef test_same_round_multiple_findings_same_topic_counts_once():\n \"\"\"Recurrence is measured in distinct ROUNDS a topic appears in, not raw\n finding count -- five credential findings in one round is one round of\n evidence, not five, otherwise a single verbose round could trip the\n threshold on its own.\"\"\"\n entries = [\n {\n \"round\": 1,\n \"findings\": [\n \"[P1] credential leak A\",\n \"[P1] credential leak B\",\n \"[P2] credential leak C\",\n ],\n },\n {\"round\": 2, \"findings\": [\"[P1] credential leak D\"]},\n ]\n result = detect.analyze(entries, threshold=3)\n rec = next(r for r in result[\"recommendations\"] if r[\"topic\"] == \"credential-redaction\")\n assert rec[\"recurrence_count\"] == 2\n assert rec[\"recommended_action\"] == \"target\"\n\n\ndef test_real_archive_recommends_mechanism_fix_for_credential_redaction(tmp_path):\n \"\"\"Regression proof against this repo's own real archive data: this must\n reproduce the same 'revise the mechanism, not just the target' call that\n was made manually before round 5 -- derived from evidence, not asserted.\"\"\"\n archive_path = Path(__file__).parent.parent / \"docs\" / \"self-improvement-archive.jsonl\"\n entries = []\n with open(archive_path) as f:\n for line in f:\n line = line.strip()\n if line:\n entries.append(json.loads(line))\n\n result = detect.analyze(entries, threshold=3)\n rec = next(\n (r for r in result[\"recommendations\"] if r[\"topic\"] == \"credential-redaction\"), None\n )\n assert rec is not None, \"expected credential-redaction topic to appear in the real archive\"\n assert rec[\"recommended_action\"] == \"mechanism\"\n assert rec[\"recurrence_count\"] >= 3\n\n\ndef test_main_cli_runs_against_a_file_and_exits_zero(tmp_path):\n archive = tmp_path / \"archive.jsonl\"\n archive.write_text(\n json.dumps({\"round\": 1, \"findings\": [\"[P1] credential leak\"]})\n + \"\\n\"\n + json.dumps({\"round\": 2, \"findings\": [\"[P1] credential leak again\"]})\n + \"\\n\"\n )\n exit_code = detect.main([\"detect-recurring-pattern.py\", str(archive), \"--threshold\", \"2\"])\n assert exit_code == 0\n{\"round\": 1, \"target\": \".github/workflows/codex-review.yml\", \"proposal\": \"Wire Codex as an automated PR reviewer (backlog item #4): run codex exec against every PR diff, post findings as a comment, fail the check on a [P1].\", \"proposed_by\": \"claude-sonnet-5\", \"validator\": \"codex (gpt-6-astra, real live execution via user's ChatGPT subscription)\", \"validation_method\": \"actual CI run on PR #2 (github.com/gagan114662/open-inspect-72e95a/pull/2) using the real CODEX_AUTH_JSON secret\", \"result\": \"rejected_with_findings\", \"findings\": [\"[P1] Review credentials are readable by the agent reviewing untrusted code — same-repo PR could prompt-inject exfiltration of auth.json into the posted comment.\", \"[P2] Failed reviews (crash/timeout/auth error) were swallowed and reported as a passing check.\", \"[P2] Prompt passed as a single shell argument; large diffs would hit 'Argument list too long'.\", \"[P2] Comment-posting step ran unconditionally, would fail on fork PRs whose default GITHUB_TOKEN is read-only.\", \"[P2] [P1] detection used a bare substring grep that 'No [P1] findings' would itself trigger.\"], \"commit\": null, \"kept\": false, \"occurred_at\": \"2026-09-14T15:50:28Z\"}\n{\"round\": 2, \"target\": \".github/workflows/codex-review.yml\", \"proposal\": \"Fix all 5 round-1 findings: redact the literal secret from output, fail closed on a crashed/timed-out review, pipe the prompt via stdin instead of a shell argument, skip comment-posting on fork PRs, anchor [P1] detection to the required **[P1]** format.\", \"proposed_by\": \"claude-sonnet-5\", \"validator_local\": \"mocked codex/git/timeout binaries, run against the real extracted run: script (parsed from the actual YAML, not hand-copied) for both the success and induced-failure paths\", \"validator_ci\": \"codex (gpt-6-astra, real live execution)\", \"validation_method\": \"local mock execution before push, then actual CI run on PR #2 with the real secret\", \"result\": \"rejected_with_findings\", \"findings\": [\"[P1] Redaction concatenated all credential values into one string before matching; an individual JSON field value (e.g. a bare access_token) echoed on its own would survive.\", \"[P1] actions/checkout persists the job's own GITHUB_TOKEN in git config by default, readable by the same agent — a second exfiltration path not covered by the round-1 fix.\", \"[P2] CODEX_API_KEY/OPENAI_API_KEY were exported unconditionally; Codex gives an API key precedence over stored ChatGPT auth when both are present, which could silently defeat subscription mode.\"], \"commit\": \"70c6c5ec\", \"kept\": true, \"kept_reason\": \"All 5 round-1 findings genuinely fixed and locally verified (the fix itself was correct and kept); 3 new findings were found on the *next* layer of the same file, which is round 3's proposal, not a rejection of round 2's actual changes.\", \"occurred_at\": \"2026-09-14T15:57:15Z\"}\n{\"round\": 3, \"target\": \".github/workflows/codex-review.yml\", \"proposal\": \"Fix all 3 round-2 findings: redact each individual JSON field value rather than the whole concatenated blob, set persist-credentials: false on checkout, only export the selected auth mode's env vars.\", \"proposed_by\": \"claude-sonnet-5\", \"validator_local\": \"mocked codex/git/timeout binaries, run against the real extracted run: script, with a mock auth.json containing multiple distinct token-shaped fields to prove per-field redaction\", \"validator_ci\": \"codex (gpt-6-astra, real live execution)\", \"validation_method\": \"local mock execution before push, then actual CI run on PR #2 with the real secret\", \"result\": \"rejected_with_findings\", \"findings\": [\"[P1] The 'Check for Codex credentials' step interpolates ${{ secrets.X }} directly into its generated shell script body (not via env:), so ALL configured credentials — including ones excluded from the selected mode — get baked into that step's own temp script file on disk, readable independent of the later redaction logic, which only knows about the selected mode's values.\", \"[P2] auth.json is rewritten fresh from the static secret on every run; if Codex rotates its refresh token mid-run, the next run reuses the stale value with no write-back, eventually causing auth failures.\", \"[P2] GitHub Actions invokes run: steps with `bash -e` by default (errexit); `set -uo pipefail` does not disable inherited -e, so a nonzero codex exec exit aborts the script immediately, before the exit_code=$? capture line ever runs — the round-2 'fail closed' fix does not actually execute as designed. This was invisible to local testing because the local mock harness ran plain `bash script.sh`, not `bash -e script.sh` — a real blind spot in the validation method itself, not just the code.\"], \"commit\": null, \"kept\": false, \"occurred_at\": \"2026-09-14T16:02:13Z\", \"note\": \"Round 3's third finding is itself a finding about the *validator* (local mock testing didn't replicate GitHub's actual shell invocation flags) — fixed for round 4 by testing with `bash -e` explicitly, not just plain bash.\"}\n{\"round\": 4, \"target\": \".github/workflows/codex-review.yml\", \"proposal\": \"Fix round-3's 3 findings: move secret checks to env: boolean flags (no raw values interpolated into script text), fix the errexit blind spot by using the codex exec call as an if-condition (bash exempts if-conditions from -e), add an actionable warning on auth-looking failures.\", \"proposed_by\": \"claude-sonnet-5\", \"validator_local\": \"mocked codex/git/timeout binaries, run against the real extracted run: script under bash -e explicitly this time (not plain bash) for both the induced-failure and success paths, specifically to close the exact blind spot round 3 identified in the validation method itself\", \"validator_ci\": \"codex (gpt-6-astra, real live execution)\", \"validation_method\": \"local mock execution under bash -e before push, then actual CI run on PR #2 with the real secret\", \"result\": \"rejected_with_findings\", \"findings\": [\"[P1] The failure-path `cat /tmp/codex-review-err.txt` prints Codex's raw stderr trace directly to job logs, bypassing the redaction logic entirely — that logic only runs on the success path. If Codex reads credential material before crashing/timing out, an individual token value could leak into logs unredacted.\", \"[P2] Configuring only OPENAI_API_KEY (not CODEX_API_KEY) …9981 tokens truncated…ked`\nL195: - `category_changed`\nL196: - `answered`\nL197: - `unanswered` | Last commit on default branch | Default branch\nL198: \nL199: Note\nL200: * More than one activity type triggers this event. For information about each activity type, see cite67†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL201: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL202: * Webhook events for GitHub Discussions are currently in public preview and subject to change.\nL203: Runs your workflow when a discussion in the workflow's repository is created or modified. For activity related to comments on a discussion, use the cite14†`discussion_comment` event. For more information about discussions, see cite68†About discussions . For information about the GraphQL API, see cite69†Discussions .\nL204: \nL205: For example, you can run a workflow when a discussion has been `created`, `edited`, or `answered`.\nL206: \nL207: `on:\nL208: discussion:\nL209: types: [created, edited, answered]\nL210: `\nL211: ## cite14†`discussion_comment` L212: \nL213: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL214: --- | --- | --- | ---\nL215: cite70†`discussion_comment` | - `created`\nL216: - `edited`\nL217: - `deleted`\nL218: | Last commit on default branch | Default branch\nL219: \nL220: Note\nL221: * More than one activity type triggers this event. For information about each activity type, see cite70†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL222: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL223: * Webhook events for GitHub Discussions are currently in public preview and subject to change.\nL224: Runs your workflow when a comment on a discussion in the workflow's repository is created or modified. For activity related to a discussion as opposed to comments on the discussion, use the cite13†`discussion` event. For more information about discussions, see cite68†About discussions . For information about the GraphQL API, see cite69†Discussions .\nL225: For example, you can run a workflow when a discussion comment has been `created` or `deleted`.\nL226: \nL227: `on:\nL228: discussion_comment:\nL229: types: [created, deleted]\nL230: `\nL231: ## cite15†`fork` L232: \nL233: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL234: --- | --- | --- | ---\nL235: cite71†`fork` | Not applicable | Last commit on default branch | Default branch\nL236: \nL237: Note\nL238: \nL239: This event will only trigger a workflow run if the workflow file exists on the default branch.\nL240: \nL241: Runs your workflow when someone forks a repository. For information about the REST API, see cite72†REST API endpoints for forks .\nL242: For example, you can run a workflow when the `fork` event occurs.\nL243: \nL244: `on:\nL245: fork\nL246: `\nL247: ## cite16†`gollum` L248: \nL249: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL250: --- | --- | --- | ---\nL251: cite73†`gollum` | Not applicable | Last commit on default branch | Default branch\nL252: \nL253: Note\nL254: \nL255: This event will only trigger a workflow run if the workflow file exists on the default branch.\nL256: \nL257: Runs your workflow when someone creates or updates a Wiki page. For more information, see cite74†About wikis .\nL258: \nL259: For example, you can run a workflow when the `gollum` event occurs.\nL260: \nL261: `on:\nL262: gollum\nL263: `\nL264: ## cite17†`image_version` L265: \nL266: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL267: --- | --- | --- | ---\nL268: Not applicable | Not applicable | Last commit on default branch | Default branch\nL269: \nL270: Runs your workflow when a new version of a specified image becomes available for use. This event is typically triggered after a successful image version creation, allowing you to automate actions such as deployment or notifications in response to new image versions.\nL271: This event supports glob patterns for both image names and versions. The example below triggers when a new image version matches any of the specified name and version combinations. For example, `[\"MyNewImage\", 1.0.0]`, `[\"MyNewImage\", 2.53.0]`, `[\"MyOtherImage\", 1.0.0]`, and `[\"MyOtherImage\", 2.0.0]`.\nL272: \nL273: `on:\nL274: image_version:\nL275: names:\nL276: - \"MyNewImage\"\nL277: - \"MyOtherImage\"\nL278: versions:\nL279: - 1.*\nL280: - 2.*\nL281: `\nL282: ## cite18†`issue_comment` L283: \nL284: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL285: --- | --- | --- | ---\nL286: cite75†`issue_comment` | - `created`\nL287: - `edited`\nL288: - `deleted`\nL289: | Last commit on default branch | Default branch\nL290: \nL291: Note\nL292: * More than one activity type triggers this event. For information about each activity type, see cite75†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL293: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL294: Runs your workflow when an issue or pull request comment is created, edited, or deleted. For information about the issue comment APIs, see cite76†Issues in the GraphQL API documentation or cite75†Webhook events and payloads in the REST API documentation.\nL295: \nL296: For example, you can run a workflow when an issue or pull request comment has been `created` or `deleted`.\nL297: \nL298: `on:\nL299: issue_comment:\nL300: types: [created, deleted]\nL301: `\nL302: ### cite77†`issue_comment` on issues only or pull requests only L303: \nL304: The `issue_comment` event occurs for comments on both issues and pull requests. You can use the `github.event.issue.pull_request` property in a conditional to take different action depending on whether the triggering object was an issue or pull request.\nL305: For example, this workflow will run the `pr_commented` job only if the `issue_comment` event originated from a pull request. It will run the `issue_commented` job only if the `issue_comment` event originated from an issue.\nL306: \nL307: `on: issue_comment\nL308: \nL309: jobs:\nL310: pr_commented:\nL311: # This job only runs for pull request comments\nL312: name: PR comment\nL313: if: ${{ github.event.issue.pull_request }}\nL314: runs-on: ubuntu-latest\nL315: steps:\nL316: - run: |\nL317: echo A comment on PR $NUMBER\nL318: env:\nL319: NUMBER: ${{ github.event.issue.number }}\nL320: \nL321: issue_commented:\nL322: # This job only runs for issue comments\nL323: name: Issue comment\nL324: if: ${{ !github.event.issue.pull_request }}\nL325: runs-on: ubuntu-latest\nL326: steps:\nL327: - run: |\nL328: echo A comment on issue $NUMBER\nL329: env:\nL330: NUMBER: ${{ github.event.issue.number }}\nL331: `\nL332: ## cite19†`issues` L333: \nL334: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL335: --- | --- | --- | ---\nL336: cite78†`issues` | - `opened`\nL337: - `edited`\nL338: - `deleted`\nL339: - `transferred`\nL340: - `pinned`\nL341: - `unpinned`\nL342: - `closed`\nL343: - `reopened`\nL344: - `assigned`\nL345: - `unassigned`\nL346: - `labeled`\nL347: - `unlabeled`\nL348: - `locked`\nL349: - `unlocked`\nL350: - `milestoned`\nL351: - `demilestoned`\nL352: - `typed`\nL353: - `untyped`\nL354: - `field_added`\nL355: - `field_removed` | Last commit on default branch | Default branch\nL356: \nL357: Note\nL358: * More than one activity type triggers this event. For information about each activity type, see cite78†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL359: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL360: Runs your workflow when an issue in the workflow's repository is created or modified. For activity related to comments in an issue, use the cite18†`issue_comment` event. For more information about issues, see cite79†About issues . For information about the issue APIs, see cite80†Issues in the GraphQL API documentation or cite81†REST API endpoints for issues .\nL361: For example, you can run a workflow when an issue has been `opened`, `edited`, or `milestoned`.\nL362: \nL363: `on:\nL364: issues:\nL365: types: [opened, edited, milestoned]\nL366: `\nL367: You can also run a workflow when an issue field value is set, changed, or cleared. The `field_added` activity type fires both when a field value is initially set and when an existing value is updated. The `field_removed` activity type fires when a field value is cleared.\nL368: \nL369: `on:\nL370: issues:\nL371: types: [field_added, field_removed]\nL372: `\nL373: ## cite20†`label` L374: \nL375: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL376: --- | --- | --- | ---\nL377: cite82†`label` | - `created`\nL378: - `edited`\nL379: - `deleted`\nL380: | Last commit on default branch | Default branch\nL381: \nL382: Note\nL383: * More than one activity type triggers this event. For information about each activity type, see cite82†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL384: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL385: Runs your workflow when a label in your workflow's repository is created or modified. For more information about labels, see cite83†Managing labels . For information about the label APIs, see cite84†Issues in the GraphQL API documentation or cite85†REST API endpoints for labels .\nL386: If you want to run your workflow when a label is added to or removed from an issue, pull request, or discussion, use the `labeled` or `unlabeled` activity types for the cite19†`issues` , cite25†`pull_request` , cite29†`pull_request_target` , or cite13†`discussion` events instead.\nL387: \nL388: For example, you can run a workflow when a label has been `created` or `deleted`.\nL389: \nL390: `on:\nL391: label:\nL392: types: [created, deleted]\nL393: `\nL394: ## cite21†`merge_group` L395: \nL396: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL397: --- | --- | --- | ---\nL398: cite86†`merge_group` | `checks_requested` | SHA of the merge group | Ref of the merge group\nL399: \nL400: Note\nL401: * More than one activity type triggers this event. Although only the `checks_requested` activity type is supported, specifying the activity type will keep your workflow specific if more activity types are added in the future. For information about each activity type, see cite86†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword.\nL402: For more information, see cite44†Workflow syntax for GitHub Actions .\nL403: * If your repository uses GitHub Actions to perform required checks on pull requests in your repository, you need to update the workflows to include the `merge_group` event as an additional trigger. Otherwise, status checks will not be triggered when you add a pull request to a merge queue. The merge will fail as the required status check will not be reported. The `merge_group` event is separate from the `pull_request` and `push` events.\nL404: Runs your workflow when a pull request is added to a merge queue, which adds the pull request to a merge group. For more information see cite87†Merging a pull request with a merge queue .\nL405: \nL406: For example, you can run a workflow when the `checks_requested` activity has occurred.\nL407: \nL408: `on:\nL409: pull_request:\nL410: branches: [ \"main\" ]\nL411: merge_group:\nL412: types: [checks_requested]\nL413: `\nL414: ## cite22†`milestone` L415: \nL416: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL417: --- | --- | --- | ---\nL418: cite88†`milestone` | - `created`\nL419: - `closed`\nL420: - `opened`\nL421: - `edited`\nL422: - `deleted`\nL423: | Last commit on default branch | Default branch\nL424: \nL425: Note\nL426: * More than one activity type triggers this event. For information about each activity type, see cite88†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL427: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL428: Runs your workflow when a milestone in the workflow's repository is created or modified. For more information about milestones, see cite89†About milestones . For information about the milestone APIs, see cite90†Issues in the GraphQL API documentation or cite91†REST API endpoints for milestones .\nL429: \nL430: If you want to run your workflow when an issue is added to or removed from a milestone, use the `milestoned` or `demilestoned` activity types for the cite19†`issues` event instead.\nL431: For example, you can run a workflow when a milestone has been `opened` or `deleted`.\nL432: \nL433: `on:\nL434: milestone:\nL435: types: [opened, deleted]\nL436: `\nL437: ## cite23†`page_build` L438: \nL439: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL440: --- | --- | --- | ---\nL441: cite92†`page_build` | Not applicable | Last commit on default branch | Default branch\nL442: \nL443: Note\nL444: \nL445: This event will only trigger a workflow run if the workflow file exists on the default branch.\nL446: Runs your workflow when someone pushes to a branch that is the publishing source for GitHub Pages, if GitHub Pages is enabled for the repository. For more information about GitHub Pages publishing sources, see cite93†Configuring a publishing source for your GitHub Pages site . For information about the REST API, see cite94†REST API endpoints for repositories .\nL447: \nL448: For example, you can run a workflow when the `page_build` event occurs.\nL449: \nL450: `on:\nL451: page_build\nL452: `\nL453: ## cite24†`public` L454: \nL455: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL456: --- | --- | --- | ---\nL457: cite95†`public` | Not applicable | Last commit on default branch | Default branch\nL458: \nL459: Note\nL460: \nL461: This event will only trigger a workflow run if the workflow file exists on the default branch.\nL462: \nL463: Runs your workflow when your workflow's repository changes from private to public. For information about the REST API, see cite96†REST API endpoints for repositories .\nL464: For example, you can run a workflow when the `public` event occurs.\nL465: \nL466: `on:\nL467: public\nL468: `\nL469: ## cite25†`pull_request` L470: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL471: --- | --- | --- | ---\nL472: cite97†`pull_request` | - `assigned`\nL473: - `unassigned`\nL474: - `labeled`\nL475: - `unlabeled`\nL476: - `opened`\nL477: - `edited`\nL478: - `closed`\nL479: - `reopened`\nL480: - `synchronize`\nL481: - `converted_to_draft`\nL482: - `locked`\nL483: - `unlocked`\nL484: - `enqueued`\nL485: - `dequeued`\nL486: - `milestoned`\nL487: - `demilestoned`\nL488: - `ready_for_review`\nL489: - `review_requested`\nL490: - `review_request_removed`\nL491: - `auto_merge_enabled`\nL492: - `auto_merge_disabled` | Last merge commit on the `GITHUB_REF` branch | PR merge branch `refs/pull/PULL_REQUEST_NUMBER/merge`\nL493: Note\nL494: * More than one activity type triggers this event. For information about each activity type, see cite97†Webhook events and payloads . By default, a workflow only runs when a `pull_request` event's activity type is `opened`, `synchronize`, or `reopened`. To trigger workflows by different activity types, use the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL495: * Workflows will not run on `pull_request` activity if the pull request has a merge conflict. The merge conflict must be resolved first. Conversely, workflows with the `pull_request_target` event will run even if the pull request has a merge conflict. Before using the `pull_request_target` trigger, you should be aware of the security risks. For more information, see cite29†`pull_request_target` .\nL496: * The `pull_request` webhook event payload is empty for merged pull requests and pull requests that come from forked repositories.\nL497: * When a pull request is created or updated by a workflow using `GITHUB_TOKEN`, `pull_request` events with the `opened`, `synchronize`, or `reopened` activity types create workflow runs that require approval. A user with write access to the repository can approve these runs from the pull request page. With the exception of `workflow_dispatch` and `repository_dispatch`, other `GITHUB_TOKEN`-triggered events do not create workflow runs at all.\nL498: * The value of `GITHUB_REF` varies for a closed pull request depending on whether the pull request has been merged or not. If a pull request was closed but not merged, it will be `refs/pull/PULL_REQUEST_NUMBER/merge`. If a pull request was closed as a result of being merged, it will be the fully qualified `ref` of the branch it was merged into, for example `/refs/heads/main`.\n--------------------------------------------------------------------------------\ncheckout/src/input-helper.ts at v4 · actions/checkout · GitHub (https://github.com/actions/checkout/blob/v4/src/input-helper.ts)\nciteturn0view1 [wordlim: 200] Crawled: today; Content type: text/html; Source: open({\"ref_id\":\"https://github.com/actions/checkout/blob/v4/src/input-helper.ts\",\"lineno\":null}); Total lines: 939\n"}}{"i":0,"result":{"status":"fulfilled","value":{"chunk_id":"b54d27","wall_time_seconds":6.25e-7,"exit_code":0,"original_token_count":276,"output":"# 网页阅读\n\n通用网页、RSS。\n\n## 通用网页 (Jina Reader)\n\n```bash\n# 读取任意网页内容\ncurl -s \"https://r.jina.ai/URL\"\n\n# 示例\ncurl -s \"https://r.jina.ai/https://example.com/article\"\n```\n\n**适用场景**: 大多数网页可以直接用 Jina Reader 读取。\n\n## Web Reader (MCP)\n\n```bash\n# 读取网页内容 (Markdown 格式)\nmcporter call 'web-reader.webReader(url: \"https://example.com\")'\n\n# 保留图片\nmcporter call 'web-reader.webReader(url: \"https://example.com\", retain_images: true)'\n\n# 纯文本格式\nmcporter call 'web-reader.webReader(url: \"https://example.com\", return_format: \"text\")'\n```\n\n**适用场景**: 需要更精确控制输出格式时使用。\n\n## RSS (feedparser)\n\n```python\npython3 -c \"\nimport feedparser\nfor e in feedparser.parse('FEED_URL').entries[:5]:\n print(f'{e.title} — {e.link}')\n\"\n```\n\n**适用场景**: 订阅博客、新闻源、播客等 RSS feed。\n\n## 选择指南\n\n| 场景 | 推荐工具 |\n|-----|---------|\n| 通用网页 | Jina Reader (`curl r.jina.ai`) |\n| 需要图片/格式控制 | web-reader MCP |\n| RSS 订阅 | feedparser |\n"}}}
{"i":1,"result":{"status":"fulfilled","value":{"chunk_id":"8e852e","wall_time_seconds":0.00000275,"exit_code":0,"original_token_count":11279,"output":"#!/usr/bin/env python3\n\"\"\"Parse [P1]/[P2] findings out of a Codex review comment's raw text.\n\nExtracted as its own tested tool because the next piece of this archive\n(the workflow that reads a completed Codex review and decides, on its own,\nwhether the recurrence pattern warrants opening a tracking issue) needs a\nreliable, reusable way to turn free-form review prose back into a list of\nindividual findings -- the same shape docs/self-improvement-archive.jsonl\nalready stores per round.\n\nFindings in this archive's own convention start a line with a number, a\nperiod, and a **[P1]** or **[P2]** marker, e.g.:\n 1. **[P1]** Some critical issue description.\nA finding may span multiple lines until the next numbered marker or the end\nof the text; this parser keeps only the first line to match how findings\nare already recorded in the archive (short, single-line summaries).\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport re\nimport sys\n\nFINDING_LINE_RE = re.compile(r\"^\\s*\\d+\\.\\s*(\\*\\*\\[(P1|P2)\\]\\*\\*.*)$\")\n\n\ndef parse_findings(text: str) -> list[str]:\n findings = []\n for line in text.splitlines():\n match = FINDING_LINE_RE.match(line)\n if match:\n findings.append(match.group(1).strip())\n return findings\n\n\ndef main(argv: list[str]) -> int:\n if len(argv) == 2:\n with open(argv[1]) as f:\n text = f.read()\n elif len(argv) == 1:\n text = sys.stdin.read()\n else:\n print(\"usage: parse-review-findings.py [file] (reads stdin if omitted)\", file=sys.stderr)\n return 2\n\n findings = parse_findings(text)\n print(json.dumps(findings, indent=2))\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main(sys.argv))\n\"\"\"Tests for analyze-latest-review.py.\n\nRun with: python3 -m pytest scripts/analyze_latest_review_test.py -q\n\"\"\"\n\nimport importlib.util\nimport json\nimport sys\nfrom pathlib import Path\n\n_MODULE_PATH = Path(__file__).parent / \"analyze-latest-review.py\"\n_spec = importlib.util.spec_from_file_location(\"analyze_latest_review\", _MODULE_PATH)\nassert _spec is not None and _spec.loader is not None\nanalyze_mod = importlib.util.module_from_spec(_spec)\nsys.modules[\"analyze_latest_review\"] = analyze_mod\n_spec.loader.exec_module(analyze_mod)\n\n\ndef _entry(round_num, findings):\n return {\"round\": round_num, \"findings\": findings}\n\n\ndef test_topic_not_yet_crossed_and_new_round_pushes_it_over():\n \"\"\"Two prior rounds mention credential redaction (below threshold 3);\n a new round's findings supply the third -> must be reported as newly\n crossed.\"\"\"\n archive = [\n _entry(1, [\"**[P1]** Secret token leaked in stdout.\"]),\n _entry(2, [\"**[P2]** Credential redaction missed a field.\"]),\n ]\n new_findings = [\"**[P1]** Another secret exposed in stderr.\"]\n\n crossed = analyze_mod.find_newly_crossed_topics(archive, new_findings, threshold=3)\n topics = {c[\"topic\"] for c in crossed}\n assert \"credential-redaction\" in topics\n\n\ndef test_topic_already_at_mechanism_level_is_not_reported_again():\n \"\"\"A topic that already recommended 'mechanism' in the archive alone\n must NOT be reported every subsequent round -- only the round that\n first tips it over counts as 'newly crossed'.\"\"\"\n archive = [\n _entry(1, [\"**[P1]** Secret leaked.\"]),\n _entry(2, [\"**[P2]** Credential redaction missed a field.\"]),\n _entry(3, [\"**[P1]** Token exposed again.\"]),\n ]\n # Already at/above threshold 3 without the new round.\n new_findings = [\"**[P2]** Yet another credential leak.\"]\n\n crossed = analyze_mod.find_newly_crossed_topics(archive, new_findings, threshold=3)\n assert crossed == []\n\n\ndef test_unrelated_new_finding_does_not_falsely_cross():\n archive = [\n _entry(1, [\"**[P1]** Secret leaked.\"]),\n _entry(2, [\"**[P2]** Credential redaction missed a field.\"]),\n ]\n new_findings = [\"**[P2]** Minor typo in a comment.\"]\n\n crossed = analyze_mod.find_newly_crossed_topics(archive, new_findings, threshold=3)\n assert crossed == []\n\n\ndef test_empty_archive_with_new_round_below_threshold_reports_nothing():\n crossed = analyze_mod.find_newly_crossed_topics([], [\"**[P1]** Secret leaked once.\"], threshold=3)\n assert crossed == []\n\n\ndef test_next_round_number_from_empty_archive_is_one():\n assert analyze_mod.next_round_number([]) == 1\n\n\ndef test_next_round_number_increments_from_max():\n archive = [_entry(1, []), _entry(4, []), _entry(2, [])]\n assert analyze_mod.next_round_number(archive) == 5\n\n\ndef test_main_cli_with_real_archive_and_no_findings_in_review(tmp_path, capsys):\n archive_path = tmp_path / \"archive.jsonl\"\n archive_path.write_text(\n json.dumps(_entry(1, [\"**[P1]** Secret leaked.\"])) + \"\\n\"\n )\n review_path = tmp_path / \"review.txt\"\n review_path.write_text(\"### Codex independent review\\n\\nNo issues found.\\n\")\n\n exit_code = analyze_mod.main(\n [\"analyze-latest-review.py\", str(archive_path), str(review_path)]\n )\n assert exit_code == 0\n out = capsys.readouterr().out\n assert '\"newly_crossed\": []' in out\n\n\ndef test_main_cli_reports_newly_crossed_topic(tmp_path, capsys):\n archive_path = tmp_path / \"archive.jsonl\"\n lines = [\n json.dumps(_entry(1, [\"**[P1]** Secret token leaked in stdout.\"])),\n json.dumps(_entry(2, [\"**[P2]** Credential redaction missed a field.\"])),\n ]\n archive_path.write_text(\"\\n\".join(lines) + \"\\n\")\n\n review_path = tmp_path / \"review.txt\"\n review_path.write_text(\n \"1. **[P1]** Another secret exposed in stderr on failure.\\n\"\n )\n\n exit_code = analyze_mod.main(\n [\"analyze-latest-review.py\", str(archive_path), str(review_path)]\n )\n assert exit_code == 0\n out = capsys.readouterr().out\n payload = json.loads(out.split(\"---\\n\", 1)[1])\n topics = {c[\"topic\"] for c in payload[\"newly_crossed\"]}\n assert \"credential-redaction\" in topics\n\"\"\"Tests for detect-recurring-pattern.py.\n\nRun with: python3 -m pytest scripts/detect_recurring_pattern_test.py -q\n\"\"\"\n\nimport importlib.util\nimport json\nimport sys\nfrom pathlib import Path\n\n_MODULE_PATH = Path(__file__).parent / \"detect-recurring-pattern.py\"\n_spec = importlib.util.spec_from_file_location(\"detect_recurring_pattern\", _MODULE_PATH)\nassert _spec is not None and _spec.loader is not None\ndetect = importlib.util.module_from_spec(_spec)\nsys.modules[\"detect_recurring_pattern\"] = detect\n_spec.loader.exec_module(detect)\n\n\ndef test_classify_finding_matches_known_topics():\n assert detect.classify_finding(\"[P1] Redact the leaked credential\") == \"credential-redaction\"\n assert detect.classify_finding(\"bash -e aborts before exit code capture\") == \"shell-semantics\"\n # Deliberately avoids the word \"token\": credential-redaction's keyword\n # list includes \"token\" too, and is checked first, so any example\n # mentioning GITHUB_TOKEN would match there instead — reasonably, since\n # GITHUB_TOKEN genuinely is credential-adjacent. This example isolates\n # fork-pr-permissions specifically.\n assert (\n detect.classify_finding(\"Fork-originated pull requests cannot receive posted comments\")\n == \"fork-pr-permissions\"\n )\n # Same reasoning: avoids \"credential\" (which would match\n # credential-redaction first) to isolate auth-lifecycle specifically.\n assert (\n detect.classify_finding(\"auth.json rotates and the old value goes stale after expiring\")\n == \"auth-lifecycle\"\n )\n\n\ndef test_credential_redaction_keyword_wins_over_other_topics_when_both_present():\n \"\"\"Documents the real, reasonable behavior the fixed test above works\n around: a finding mentioning GITHUB_TOKEN is credential-adjacent, so it\n is classified as credential-redaction even when it's really about fork\n permissions specifically. Topic buckets are approximate by design (see\n module docstring) — this pins the actual priority order rather than\n leaving it as an implicit, undocumented side effect.\"\"\"\n assert (\n detect.classify_finding(\"Fork PRs fail: GITHUB_TOKEN read-only\")\n == \"credential-redaction\"\n )\n\n\ndef test_classify_finding_returns_none_for_unmatched_text():\n assert detect.classify_finding(\"this finding matches no known topic at all\") is None\n\n\ndef test_recommends_mechanism_fix_once_topic_recurs_at_threshold():\n entries = [\n {\"round\": 1, \"findings\": [\"[P1] leaked credential in output\"]},\n {\"round\": 2, \"findings\": [\"[P1] secret token exposed again\"]},\n {\"round\": 3, \"findings\": [\"[P2] another credential redaction gap\"]},\n ]\n result = detect.analyze(entries, threshold=3)\n rec = next(r for r in result[\"recommendations\"] if r[\"topic\"] == \"credential-redaction\")\n assert rec[\"recurrence_count\"] == 3\n assert rec[\"recommended_action\"] == \"mechanism\"\n assert rec[\"rounds\"] == [1, 2, 3]\n\n\ndef test_recommends_target_fix_below_threshold():\n entries = [\n {\"round\": 1, \"findings\": [\"[P1] leaked credential in output\"]},\n {\"round\": 2, \"findings\": [\"[P1] secret token exposed again\"]},\n ]\n result = detect.analyze(entries, threshold=3)\n rec = next(r for r in result[\"recommendations\"] if r[\"topic\"] == \"credential-redaction\")\n assert rec[\"recurrence_count\"] == 2\n assert rec[\"recommended_action\"] == \"target\"\n\n\ndef test_same_round_multiple_findings_same_topic_counts_once():\n \"\"\"Recurrence is measured in distinct ROUNDS a topic appears in, not raw\n finding count -- five credential findings in one round is one round of\n evidence, not five, otherwise a single verbose round could trip the\n threshold on its own.\"\"\"\n entries = [\n {\n \"round\": 1,\n \"findings\": [\n \"[P1] credential leak A\",\n \"[P1] credential leak B\",\n \"[P2] credential leak C\",\n ],\n },\n {\"round\": 2, \"findings\": [\"[P1] credential leak D\"]},\n ]\n result = detect.analyze(entries, threshold=3)\n rec = next(r for r in result[\"recommendations\"] if r[\"topic\"] == \"credential-redaction\")\n assert rec[\"recurrence_count\"] == 2\n assert rec[\"recommended_action\"] == \"target\"\n\n\ndef test_real_archive_recommends_mechanism_fix_for_credential_redaction(tmp_path):\n \"\"\"Regression proof against this repo's own real archive data: this must\n reproduce the same 'revise the mechanism, not just the target' call that\n was made manually before round 5 -- derived from evidence, not asserted.\"\"\"\n archive_path = Path(__file__).parent.parent / \"docs\" / \"self-improvement-archive.jsonl\"\n entries = []\n with open(archive_path) as f:\n for line in f:\n line = line.strip()\n if line:\n entries.append(json.loads(line))\n\n result = detect.analyze(entries, threshold=3)\n rec = next(\n (r for r in result[\"recommendations\"] if r[\"topic\"] == \"credential-redaction\"), None\n )\n assert rec is not None, \"expected credential-redaction topic to appear in the real archive\"\n assert rec[\"recommended_action\"] == \"mechanism\"\n assert rec[\"recurrence_count\"] >= 3\n\n\ndef test_main_cli_runs_against_a_file_and_exits_zero(tmp_path):\n archive = tmp_path / \"archive.jsonl\"\n archive.write_text(\n json.dumps({\"round\": 1, \"findings\": [\"[P1] credential leak\"]})\n + \"\\n\"\n + json.dumps({\"round\": 2, \"findings\": [\"[P1] credential leak again\"]})\n + \"\\n\"\n )\n exit_code = detect.main([\"detect-recurring-pattern.py\", str(archive), \"--threshold\", \"2\"])\n assert exit_code == 0\n{\"round\": 1, \"target\": \".github/workflows/codex-review.yml\", \"proposal\": \"Wire Codex as an automated PR reviewer (backlog item #4): run codex exec against every PR diff, post findings as a comment, fail the check on a [P1].\", \"proposed_by\": \"claude-sonnet-5\", \"validator\": \"codex (gpt-6-astra, real live execution via user's ChatGPT subscription)\", \"validation_method\": \"actual CI run on PR #2 (github.com/gagan114662/open-inspect-72e95a/pull/2) using the real CODEX_AUTH_JSON secret\", \"result\": \"rejected_with_findings\", \"findings\": [\"[P1] Review credentials are readable by the agent reviewing untrusted code — same-repo PR could prompt-inject exfiltration of auth.json into the posted comment.\", \"[P2] Failed reviews (crash/timeout/auth error) were swallowed and reported as a passing check.\", \"[P2] Prompt passed as a single shell argument; large diffs would hit 'Argument list too long'.\", \"[P2] Comment-posting step ran unconditionally, would fail on fork PRs whose default GITHUB_TOKEN is read-only.\", \"[P2] [P1] detection used a bare substring grep that 'No [P1] findings' would itself trigger.\"], \"commit\": null, \"kept\": false, \"occurred_at\": \"2026-09-14T15:50:28Z\"}\n{\"round\": 2, \"target\": \".github/workflows/codex-review.yml\", \"proposal\": \"Fix all 5 round-1 findings: redact the literal secret from output, fail closed on a crashed/timed-out review, pipe the prompt via stdin instead of a shell argument, skip comment-posting on fork PRs, anchor [P1] detection to the required **[P1]** format.\", \"proposed_by\": \"claude-sonnet-5\", \"validator_local\": \"mocked codex/git/timeout binaries, run against the real extracted run: script (parsed from the actual YAML, not hand-copied) for both the success and induced-failure paths\", \"validator_ci\": \"codex (gpt-6-astra, real live execution)\", \"validation_method\": \"local mock execution before push, then actual CI run on PR #2 with the real secret\", \"result\": \"rejected_with_findings\", \"findings\": [\"[P1] Redaction concatenated all credential values into one string before matching; an individual JSON field value (e.g. a bare access_token) echoed on its own would survive.\", \"[P1] actions/checkout persists the job's own GITHUB_TOKEN in git config by default, readable by the same agent — a second exfiltration path not covered by the round-1 fix.\", \"[P2] CODEX_API_KEY/OPENAI_API_KEY were exported unconditionally; Codex gives an API key precedence over stored ChatGPT auth when both are present, which could silently defeat subscription mode.\"], \"commit\": \"70c6c5ec\", \"kept\": true, \"kept_reason\": \"All 5 round-1 findings genuinely fixed and locally verified (the fix itself was correct and kept); 3 new findings were found on the *next* layer of the same file, which is round 3's proposal, not a rejection of round 2's actual changes.\", \"occurred_at\": \"2026-09-14T15:57:15Z\"}\n{\"round\": 3, \"target\": \".github/workflows/codex-review.yml\", \"proposal\": \"Fix all 3 round-2 findings: redact each individual JSON field value rather than the whole concatenated blob, set persist-credentials: false on checkout, only export the selected auth mode's env vars.\", \"proposed_by\": \"claude-sonnet-5\", \"validator_local\": \"mocked codex/git/timeout binaries, run against the real extracted run: script, with a mock auth.json containing multiple distinct token-shaped fields to prove per-field redaction\", \"validator_ci\": \"codex (gpt-6-astra, real live execution)\", \"validation_method\": \"local mock execution before push, then actual CI run on PR #2 with the real secret\", \"result\": \"rejected_with_findings\", \"findings\": [\"[P1] The 'Check for Codex credentials' step interpolates ${{ secrets.X }} directly into its generated shell script body (not via env:), so ALL configured credentials — including ones excluded from the selected mode — get baked into that step's own temp script file on disk, readable independent of the later redaction logic, which only knows about the selected mode's values.\", \"[P2] auth.json is rewritten fresh from the static secret on every run; if Codex rotates its refresh token mid-run, the next run reuses the stale value with no write-back, eventually causing auth failures.\", \"[P2] GitHub Actions invokes run: steps with `bash -e` by default (errexit); `set -uo pipefail` does not disable inherited -e, so a nonzero codex exec exit aborts the script immediately, before the exit_code=$? capture line ever runs — the round-2 'fail closed' fix does not actually execute as designed. This was invisible to local testing because the local mock harness ran plain `bash script.sh`, not `bash -e script.sh` — a real blind spot in the validation method itself, not just the code.\"], \"commit\": null, \"kept\": false, \"occurred_at\": \"2026-09-14T16:02:13Z\", \"note\": \"Round 3's third finding is itself a finding about the *validator* (local mock testing didn't replicate GitHub's actual shell invocation flags) — fixed for round 4 by testing with `bash -e` explicitly, not just plain bash.\"}\n{\"round\": 4, \"target\": \".github/workflows/codex-review.yml\", \"proposal\": \"Fix round-3's 3 findings: move secret checks to env: boolean flags (no raw values interpolated into script text), fix the errexit blind spot by using the codex exec call as an if-condition (bash exempts if-conditions from -e), add an actionable warning on auth-looking failures.\", \"proposed_by\": \"claude-sonnet-5\", \"validator_local\": \"mocked codex/git/timeout binaries, run against the real extracted run: script under bash -e explicitly this time (not plain bash) for both the induced-failure and success paths, specifically to close the exact blind spot round 3 identified in the validation method itself\", \"validator_ci\": \"codex (gpt-6-astra, real live execution)\", \"validation_method\": \"local mock execution under bash -e before push, then actual CI run on PR #2 with the real secret\", \"result\": \"rejected_with_findings\", \"findings\": [\"[P1] The failure-path `cat /tmp/codex-review-err.txt` prints Codex's raw stderr trace directly to job logs, bypassing the redaction logic entirely — that logic only runs on the success path. If Codex reads credential material before crashing/timing out, an individual token value could leak into logs unredacted.\", \"[P2] Configuring only OPENAI_API_KEY (not CODEX_API_KEY) …9981 tokens truncated…ked`\nL195: - `category_changed`\nL196: - `answered`\nL197: - `unanswered` | Last commit on default branch | Default branch\nL198: \nL199: Note\nL200: * More than one activity type triggers this event. For information about each activity type, see cite67†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL201: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL202: * Webhook events for GitHub Discussions are currently in public preview and subject to change.\nL203: Runs your workflow when a discussion in the workflow's repository is created or modified. For activity related to comments on a discussion, use the cite14†`discussion_comment` event. For more information about discussions, see cite68†About discussions . For information about the GraphQL API, see cite69†Discussions .\nL204: \nL205: For example, you can run a workflow when a discussion has been `created`, `edited`, or `answered`.\nL206: \nL207: `on:\nL208: discussion:\nL209: types: [created, edited, answered]\nL210: `\nL211: ## cite14†`discussion_comment` L212: \nL213: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL214: --- | --- | --- | ---\nL215: cite70†`discussion_comment` | - `created`\nL216: - `edited`\nL217: - `deleted`\nL218: | Last commit on default branch | Default branch\nL219: \nL220: Note\nL221: * More than one activity type triggers this event. For information about each activity type, see cite70†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL222: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL223: * Webhook events for GitHub Discussions are currently in public preview and subject to change.\nL224: Runs your workflow when a comment on a discussion in the workflow's repository is created or modified. For activity related to a discussion as opposed to comments on the discussion, use the cite13†`discussion` event. For more information about discussions, see cite68†About discussions . For information about the GraphQL API, see cite69†Discussions .\nL225: For example, you can run a workflow when a discussion comment has been `created` or `deleted`.\nL226: \nL227: `on:\nL228: discussion_comment:\nL229: types: [created, deleted]\nL230: `\nL231: ## cite15†`fork` L232: \nL233: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL234: --- | --- | --- | ---\nL235: cite71†`fork` | Not applicable | Last commit on default branch | Default branch\nL236: \nL237: Note\nL238: \nL239: This event will only trigger a workflow run if the workflow file exists on the default branch.\nL240: \nL241: Runs your workflow when someone forks a repository. For information about the REST API, see cite72†REST API endpoints for forks .\nL242: For example, you can run a workflow when the `fork` event occurs.\nL243: \nL244: `on:\nL245: fork\nL246: `\nL247: ## cite16†`gollum` L248: \nL249: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL250: --- | --- | --- | ---\nL251: cite73†`gollum` | Not applicable | Last commit on default branch | Default branch\nL252: \nL253: Note\nL254: \nL255: This event will only trigger a workflow run if the workflow file exists on the default branch.\nL256: \nL257: Runs your workflow when someone creates or updates a Wiki page. For more information, see cite74†About wikis .\nL258: \nL259: For example, you can run a workflow when the `gollum` event occurs.\nL260: \nL261: `on:\nL262: gollum\nL263: `\nL264: ## cite17†`image_version` L265: \nL266: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL267: --- | --- | --- | ---\nL268: Not applicable | Not applicable | Last commit on default branch | Default branch\nL269: \nL270: Runs your workflow when a new version of a specified image becomes available for use. This event is typically triggered after a successful image version creation, allowing you to automate actions such as deployment or notifications in response to new image versions.\nL271: This event supports glob patterns for both image names and versions. The example below triggers when a new image version matches any of the specified name and version combinations. For example, `[\"MyNewImage\", 1.0.0]`, `[\"MyNewImage\", 2.53.0]`, `[\"MyOtherImage\", 1.0.0]`, and `[\"MyOtherImage\", 2.0.0]`.\nL272: \nL273: `on:\nL274: image_version:\nL275: names:\nL276: - \"MyNewImage\"\nL277: - \"MyOtherImage\"\nL278: versions:\nL279: - 1.*\nL280: - 2.*\nL281: `\nL282: ## cite18†`issue_comment` L283: \nL284: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL285: --- | --- | --- | ---\nL286: cite75†`issue_comment` | - `created`\nL287: - `edited`\nL288: - `deleted`\nL289: | Last commit on default branch | Default branch\nL290: \nL291: Note\nL292: * More than one activity type triggers this event. For information about each activity type, see cite75†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL293: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL294: Runs your workflow when an issue or pull request comment is created, edited, or deleted. For information about the issue comment APIs, see cite76†Issues in the GraphQL API documentation or cite75†Webhook events and payloads in the REST API documentation.\nL295: \nL296: For example, you can run a workflow when an issue or pull request comment has been `created` or `deleted`.\nL297: \nL298: `on:\nL299: issue_comment:\nL300: types: [created, deleted]\nL301: `\nL302: ### cite77†`issue_comment` on issues only or pull requests only L303: \nL304: The `issue_comment` event occurs for comments on both issues and pull requests. You can use the `github.event.issue.pull_request` property in a conditional to take different action depending on whether the triggering object was an issue or pull request.\nL305: For example, this workflow will run the `pr_commented` job only if the `issue_comment` event originated from a pull request. It will run the `issue_commented` job only if the `issue_comment` event originated from an issue.\nL306: \nL307: `on: issue_comment\nL308: \nL309: jobs:\nL310: pr_commented:\nL311: # This job only runs for pull request comments\nL312: name: PR comment\nL313: if: ${{ github.event.issue.pull_request }}\nL314: runs-on: ubuntu-latest\nL315: steps:\nL316: - run: |\nL317: echo A comment on PR $NUMBER\nL318: env:\nL319: NUMBER: ${{ github.event.issue.number }}\nL320: \nL321: issue_commented:\nL322: # This job only runs for issue comments\nL323: name: Issue comment\nL324: if: ${{ !github.event.issue.pull_request }}\nL325: runs-on: ubuntu-latest\nL326: steps:\nL327: - run: |\nL328: echo A comment on issue $NUMBER\nL329: env:\nL330: NUMBER: ${{ github.event.issue.number }}\nL331: `\nL332: ## cite19†`issues` L333: \nL334: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL335: --- | --- | --- | ---\nL336: cite78†`issues` | - `opened`\nL337: - `edited`\nL338: - `deleted`\nL339: - `transferred`\nL340: - `pinned`\nL341: - `unpinned`\nL342: - `closed`\nL343: - `reopened`\nL344: - `assigned`\nL345: - `unassigned`\nL346: - `labeled`\nL347: - `unlabeled`\nL348: - `locked`\nL349: - `unlocked`\nL350: - `milestoned`\nL351: - `demilestoned`\nL352: - `typed`\nL353: - `untyped`\nL354: - `field_added`\nL355: - `field_removed` | Last commit on default branch | Default branch\nL356: \nL357: Note\nL358: * More than one activity type triggers this event. For information about each activity type, see cite78†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL359: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL360: Runs your workflow when an issue in the workflow's repository is created or modified. For activity related to comments in an issue, use the cite18†`issue_comment` event. For more information about issues, see cite79†About issues . For information about the issue APIs, see cite80†Issues in the GraphQL API documentation or cite81†REST API endpoints for issues .\nL361: For example, you can run a workflow when an issue has been `opened`, `edited`, or `milestoned`.\nL362: \nL363: `on:\nL364: issues:\nL365: types: [opened, edited, milestoned]\nL366: `\nL367: You can also run a workflow when an issue field value is set, changed, or cleared. The `field_added` activity type fires both when a field value is initially set and when an existing value is updated. The `field_removed` activity type fires when a field value is cleared.\nL368: \nL369: `on:\nL370: issues:\nL371: types: [field_added, field_removed]\nL372: `\nL373: ## cite20†`label` L374: \nL375: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL376: --- | --- | --- | ---\nL377: cite82†`label` | - `created`\nL378: - `edited`\nL379: - `deleted`\nL380: | Last commit on default branch | Default branch\nL381: \nL382: Note\nL383: * More than one activity type triggers this event. For information about each activity type, see cite82†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL384: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL385: Runs your workflow when a label in your workflow's repository is created or modified. For more information about labels, see cite83†Managing labels . For information about the label APIs, see cite84†Issues in the GraphQL API documentation or cite85†REST API endpoints for labels .\nL386: If you want to run your workflow when a label is added to or removed from an issue, pull request, or discussion, use the `labeled` or `unlabeled` activity types for the cite19†`issues` , cite25†`pull_request` , cite29†`pull_request_target` , or cite13†`discussion` events instead.\nL387: \nL388: For example, you can run a workflow when a label has been `created` or `deleted`.\nL389: \nL390: `on:\nL391: label:\nL392: types: [created, deleted]\nL393: `\nL394: ## cite21†`merge_group` L395: \nL396: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL397: --- | --- | --- | ---\nL398: cite86†`merge_group` | `checks_requested` | SHA of the merge group | Ref of the merge group\nL399: \nL400: Note\nL401: * More than one activity type triggers this event. Although only the `checks_requested` activity type is supported, specifying the activity type will keep your workflow specific if more activity types are added in the future. For information about each activity type, see cite86†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword.\nL402: For more information, see cite44†Workflow syntax for GitHub Actions .\nL403: * If your repository uses GitHub Actions to perform required checks on pull requests in your repository, you need to update the workflows to include the `merge_group` event as an additional trigger. Otherwise, status checks will not be triggered when you add a pull request to a merge queue. The merge will fail as the required status check will not be reported. The `merge_group` event is separate from the `pull_request` and `push` events.\nL404: Runs your workflow when a pull request is added to a merge queue, which adds the pull request to a merge group. For more information see cite87†Merging a pull request with a merge queue .\nL405: \nL406: For example, you can run a workflow when the `checks_requested` activity has occurred.\nL407: \nL408: `on:\nL409: pull_request:\nL410: branches: [ \"main\" ]\nL411: merge_group:\nL412: types: [checks_requested]\nL413: `\nL414: ## cite22†`milestone` L415: \nL416: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL417: --- | --- | --- | ---\nL418: cite88†`milestone` | - `created`\nL419: - `closed`\nL420: - `opened`\nL421: - `edited`\nL422: - `deleted`\nL423: | Last commit on default branch | Default branch\nL424: \nL425: Note\nL426: * More than one activity type triggers this event. For information about each activity type, see cite88†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL427: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL428: Runs your workflow when a milestone in the workflow's repository is created or modified. For more information about milestones, see cite89†About milestones . For information about the milestone APIs, see cite90†Issues in the GraphQL API documentation or cite91†REST API endpoints for milestones .\nL429: \nL430: If you want to run your workflow when an issue is added to or removed from a milestone, use the `milestoned` or `demilestoned` activity types for the cite19†`issues` event instead.\nL431: For example, you can run a workflow when a milestone has been `opened` or `deleted`.\nL432: \nL433: `on:\nL434: milestone:\nL435: types: [opened, deleted]\nL436: `\nL437: ## cite23†`page_build` L438: \nL439: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL440: --- | --- | --- | ---\nL441: cite92†`page_build` | Not applicable | Last commit on default branch | Default branch\nL442: \nL443: Note\nL444: \nL445: This event will only trigger a workflow run if the workflow file exists on the default branch.\nL446: Runs your workflow when someone pushes to a branch that is the publishing source for GitHub Pages, if GitHub Pages is enabled for the repository. For more information about GitHub Pages publishing sources, see cite93†Configuring a publishing source for your GitHub Pages site . For information about the REST API, see cite94†REST API endpoints for repositories .\nL447: \nL448: For example, you can run a workflow when the `page_build` event occurs.\nL449: \nL450: `on:\nL451: page_build\nL452: `\nL453: ## cite24†`public` L454: \nL455: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL456: --- | --- | --- | ---\nL457: cite95†`public` | Not applicable | Last commit on default branch | Default branch\nL458: \nL459: Note\nL460: \nL461: This event will only trigger a workflow run if the workflow file exists on the default branch.\nL462: \nL463: Runs your workflow when your workflow's repository changes from private to public. For information about the REST API, see cite96†REST API endpoints for repositories .\nL464: For example, you can run a workflow when the `public` event occurs.\nL465: \nL466: `on:\nL467: public\nL468: `\nL469: ## cite25†`pull_request` L470: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL471: --- | --- | --- | ---\nL472: cite97†`pull_request` | - `assigned`\nL473: - `unassigned`\nL474: - `labeled`\nL475: - `unlabeled`\nL476: - `opened`\nL477: - `edited`\nL478: - `closed`\nL479: - `reopened`\nL480: - `synchronize`\nL481: - `converted_to_draft`\nL482: - `locked`\nL483: - `unlocked`\nL484: - `enqueued`\nL485: - `dequeued`\nL486: - `milestoned`\nL487: - `demilestoned`\nL488: - `ready_for_review`\nL489: - `review_requested`\nL490: - `review_request_removed`\nL491: - `auto_merge_enabled`\nL492: - `auto_merge_disabled` | Last merge commit on the `GITHUB_REF` branch | PR merge branch `refs/pull/PULL_REQUEST_NUMBER/merge`\nL493: Note\nL494: * More than one activity type triggers this event. For information about each activity type, see cite97†Webhook events and payloads . By default, a workflow only runs when a `pull_request` event's activity type is `opened`, `synchronize`, or `reopened`. To trigger workflows by different activity types, use the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL495: * Workflows will not run on `pull_request` activity if the pull request has a merge conflict. The merge conflict must be resolved first. Conversely, workflows with the `pull_request_target` event will run even if the pull request has a merge conflict. Before using the `pull_request_target` trigger, you should be aware of the security risks. For more information, see cite29†`pull_request_target` .\nL496: * The `pull_request` webhook event payload is empty for merged pull requests and pull requests that come from forked repositories.\nL497: * When a pull request is created or updated by a workflow using `GITHUB_TOKEN`, `pull_request` events with the `opened`, `synchronize`, or `reopened` activity types create workflow runs that require approval. A user with write access to the repository can approve these runs from the pull request page. With the exception of `workflow_dispatch` and `repository_dispatch`, other `GITHUB_TOKEN`-triggered events do not create workflow runs at all.\nL498: * The value of `GITHUB_REF` varies for a closed pull request depending on whether the pull request has been merged or not. If a pull request was closed but not merged, it will be `refs/pull/PULL_REQUEST_NUMBER/merge`. If a pull request was closed as a result of being merged, it will be the fully qualified `ref` of the branch it was merged into, for example `/refs/heads/main`.\n--------------------------------------------------------------------------------\ncheckout/src/input-helper.ts at v4 · actions/checkout · GitHub (https://github.com/actions/checkout/blob/v4/src/input-helper.ts)\nciteturn0view1 [wordlim: 200] Crawled: today; Content type: text/html; Source: open({\"ref_id\":\"https://github.com/actions/checkout/blob/v4/src/input-helper.ts\",\"lineno\":null}); Total lines: 939\n"}}{"i":0,"result":{"status":"fulfilled","value":"Events that trigger workflows - GitHub Docs (https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows)\nciteturn1view0 [wordlim: 200] Crawled: today; Content type: text/html; Source: find({\"ref_id\":\"turn0view0\",\"pattern\":\"## workflow_run\"}); Total lines: 1321\nL1177: If you run this workflow from a browser you must enter values for the required inputs manually before the workflow will run.\nL1178: \nL1179: cite161†Image: Screenshot of a list of workflow runs. A dropdown menu, labeled \"Run workflow\" and expanded to show input fields, is outlined in dark orange. L1180: \nL1181: You can also pass inputs when you run a workflow from a script, or by using GitHub CLI. For example:\nL1182: \nL1183: `gh workflow run run-tests.yml -f logLevel=warning -f tags=false -f environment=staging\nL1184: `\nL1185: For more information, see the GitHub CLI information in cite158†Manually running a workflow .\nL1186: ## cite39†`workflow_run` L1187: \nL1188: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL1189: --- | --- | --- | ---\nL1190: cite162†`workflow_run` | - `completed`\nL1191: - `requested`\nL1192: - `in_progress` | Last commit on default branch | Default branch\nL1193: \nL1194: Note\nL1195: * More than one activity type triggers this event. The `requested` activity type does not occur when a workflow is re-run. For information about each activity type, see cite162†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL1196: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL1197: * You can't use `workflow_run` to chain together more than three levels of workflows. For example, if you attempt to trigger five workflows (named `B` to `F`) to run sequentially after an initial workflow `A` has run (that is: `A` → `B` → `C` → `D` → `E` → `F`), workflows `E` and `F` will not be run.\nL1198: This event occurs when a workflow run is requested or completed. It allows you to execute a workflow based on execution or completion of another workflow. The workflow started by the `workflow_run` event is able to access secrets and write tokens, even if the previous workflow was not. This is useful in cases where the previous workflow is intentionally not privileged, but you need to take a privileged action in a later workflow.\nL1199: \nL1200: Warning\nL1201: Running untrusted code on the `workflow_run` trigger may lead to security vulnerabilities. These vulnerabilities include cache poisoning and granting unintended access to write privileges or secrets. For more information, see cite123†Secure use reference in the GitHub Enterprise Cloud documentation, and cite124†Preventing pwn requests†securitylab.github.com on the GitHub Security Lab website.\nL1202: In this example, a workflow is configured to run after the separate \"Run Tests\" workflow completes.\nL1203: \nL1204: `on:\nL1205: workflow_run:\nL1206: workflows: [Run Tests]\nL1207: types:\nL1208: - completed\nL1209: `\nL1210: If you specify multiple `workflows` for the `workflow_run` event, only one of the workflows needs to run. For example, a workflow with the following trigger will run whenever the \"Staging\" workflow or the \"Lab\" workflow completes.\nL1211: \nL1212: `on:\nL1213: workflow_run:\nL1214: workflows: [Staging, Lab]\nL1215: types:\nL1216: - completed\nL1217: `\nL1218: ### cite163†Running a workflow based on the conclusion of another workflow L1219: A workflow run is triggered regardless of the conclusion of the previous workflow. If you want to run a job or step based on the result of the triggering workflow, you can use a conditional with the `github.event.workflow_run.conclusion` property. For example, this workflow will run whenever a workflow named \"Build\" completes, but the `on-success` job will only run if the \"Build\" workflow succeeded, and the `on-failure` job will only run if the \"Build\" workflow failed:\nL1220: \nL1221: `on:\nL1222: workflow_run:\nL1223: workflows: [Build]\nL1224: types: [completed]\nL1225: \nL1226: jobs:\nL1227: on-success:\nL1228: runs-on: ubuntu-latest\nL1229: if: ${{ github.event.workflow_run.conclusion == 'success' }}\nL1230: steps:\nL1231: - run: echo 'The triggering workflow passed'\nL1232: on-failure:\nL1233: runs-on: ubuntu-latest\nL1234: if: ${{ github.event.workflow_run.conclusion == 'failure' }}\nL1235: steps:\nL1236: - run: echo 'The triggering workflow failed'\nL1237: `\nL1238: ### cite164†Limiting your workflow to run based on branches L1239: You can use the `branches` or `branches-ignore` filter to specify what branches the triggering workflow must run on in order to trigger your workflow. For more information, see cite165†Workflow syntax for GitHub Actions . For example, a workflow with the following trigger will only run when the workflow named `Build` runs on a branch named `canary`.\nL1240: \nL1241: `on:\nL1242: workflow_run:\nL1243: workflows: [Build]\nL1244: types: [requested]\nL1245: branches: [canary]\nL1246: `\nL1247: ### cite166†Using data from the triggering workflow L1248: \nL1249: You can access the cite162†`workflow_run` event payload that corresponds to the workflow that triggered your workflow. For example, if your triggering workflow generates artifacts, a workflow triggered with the `workflow_run` event can access these artifacts.\nL1250: The following workflow uploads data as an artifact. (In this simplified example, the data is the pull request number.)\nL1251: \nL1252: `name: Upload data\nL1253: \nL1254: on:\nL1255: pull_request:\nL1256: \nL1257: jobs:\nL1258: upload:\nL1259: runs-on: ubuntu-latest\nL1260: \nL1261: steps:\nL1262: - name: Save PR number\nL1263: env:\nL1264: PR_NUMBER: ${{ github.event.number }}\nL1265: run: |\nL1266: mkdir -p ./pr\nL1267: echo $PR_NUMBER > ./pr/pr_number\nL1268: - uses: actions/upload-artifact@v4\nL1269: with:\nL1270: name: pr_number\nL1271: path: pr/\nL1272: `\nL1273: When a run of the above workflow completes, it triggers a run of the following workflow. The following workflow uses the `github.event.workflow_run` context and the actions/download-artifact@v5 action to download the artifact that was uploaded by the above workflow, then comments on the pull request whose number was uploaded as an artifact.\nL1274: \nL1275: `name: Use the data\nL1276: \nL1277: on:\nL1278: workflow_run:\nL1279: workflows: [Upload data]\nL1280: types:\nL1281: - completed\nL1282: \nL1283: jobs:\nL1284: download:\nL1285: runs-on: ubuntu-latest\nL1286: permissions:\nL1287: actions: read\nL1288: issues: write\nL1289: steps:\nL1290: - name: 'Download artifact'\nL1291: uses: actions/download-artifact@v5\nL1292: with:\nL1293: name: pr_number\nL1294: # do not extract in the workspace dir that may contain executable scripts\nL1295: path: ${{ runner.temp }}/artifacts\nL1296: run-id: ${{ github.event.workflow_run.id }}\nL1297: github-token: ${{ secrets.GITHUB_TOKEN }}\nL1298: - name: 'Comment on PR'\nL1299: uses: actions/github-script@v8\nL1300: with:\nL1301: github-token: ${{ secrets.GITHUB_TOKEN }}\nL1302: script: |\nL1303: const fs = require('fs');\nL1304: const path = require('path');\nL1305: const temp = '${{ runner.temp }}/artifacts';\nL1306: const issue_number_raw = fs.readFileSync(path.join(temp, 'pr_number'), 'utf8').trim();\nL1307: const issue_number = Number(issue_number_raw);\nL1308: if (!Number.isInteger(issue_number)) {\nL1309: throw new Error(`Invalid PR number in pr_number artifact: \"${issue_number_raw}\"`);\nL1310: }\nL1311: await github.rest.issues.createComment({\nL1312: owner: context.repo.owner,\nL1313: repo: context.repo.repo,\nL1314: issue_number: issue_number,\nL1315: body: 'Thank you for the PR!'\nL1316: });\n--------------------------------------------------------------------------------\ncheckout/src/input-helper.ts at v4 · actions/checkout · GitHub (https://github.com/actions/checkout/blob/v4/src/input-helper.ts)\nciteturn1view1 [wordlim: 200] Crawled: today; Content type: text/html; Source: find({\"ref_id\":\"turn0view1\",\"pattern\":\"result.ref = github.context.ref\"}); Total lines: 939\nL636: `${github.context.repo.owner}/${github.context.repo.repo}`\nL637: \nL638: core.debug(`qualified repository = '${qualifiedRepository}'`)\nL639: \nL640: const splitRepository = qualifiedRepository.split('/')\nL641: \nL642: if (\nL643: \nL644: splitRepository.length !== 2 ||\nL645: \nL646: !splitRepository[0] ||\nL647: \nL648: !splitRepository[1]\nL649: \nL650: ) {\nL651: \nL652: throw new Error(\nL653: \nL654: `Invalid repository '${qualifiedRepository}'. Expected format {owner}/{repo}.`\nL655: \nL656: )\nL657: \nL658: }\nL659: \nL660: result.repositoryOwner = splitRepository[0]\nL661: \nL662: result.repositoryName = splitRepository[1]\nL663: \nL664: // Repository path\nL665: result.repositoryPath = core.getInput('path') || '.'\nL666: \nL667: result.repositoryPath = path.resolve(\nL668: \nL669: githubWorkspacePath,\nL670: \nL671: result.repositoryPath\nL672: \nL673: )\nL674: \nL675: if (\nL676: \nL677: !(result.repositoryPath + path.sep).startsWith(\nL678: \nL679: githubWorkspacePath + path.sep\nL680: \nL681: )\nL682: \nL683: ) {\nL684: \nL685: throw new Error(\nL686: \nL687: `Repository path '${result.repositoryPath}' is not under '${githubWorkspacePath}'`\nL688: \nL689: )\nL690: \nL691: }\nL692: \nL693: // Workflow repository?\nL694: \nL695: const isWorkflowRepository =\nL696: \nL697: qualifiedRepository.toUpperCase() ===\nL698: \nL699: `${github.context.repo.owner}/${github.context.repo.repo}`.toUpperCase()\nL700: // Source branch, source version\nL701: \nL702: result.ref = core.getInput('ref')\nL703: \nL704: // core.getInput()'s default trim strips a range of Unicode characters such as a\nL705: \nL706: // leading BOM (U+FEFF) or NBSP (U+00A0). Those are valid in a git ref name, so\nL707: \nL708: // a fork branch named \"<BOM>\" + 40 hex chars would trim down to a bare SHA and\nL709: \nL710: // be silently reclassified as a commit, bypassing the unsafe fork PR checkout\nL711: \nL712: // guard.\nL713: \nL714: //\nL715: \nL716: // The trim below strips only the ASCII whitespace characters which are all forbidden\nL717: // in a git branch name.\nL718: \nL719: // \\t U+0009 horizontal tab - ASCII control, forbidden in ref names\nL720: \nL721: // \\n U+000A line feed - ASCII control, forbidden in ref names\nL722: \nL723: // \\v U+000B vertical tab - ASCII control, forbidden in ref names\nL724: \nL725: // \\f U+000C form feed - ASCII control, forbidden in ref names\nL726: \nL727: // \\r U+000D carriage return - ASCII control, forbidden in ref names\nL728: \nL729: // ' ' U+0020 space - forbidden in ref names\nL730: \nL731: const asciiTrimmedRef = core\nL732: \nL733: .getInput('ref', {trimWhitespace: false})\nL734: .replace(/^[\\t\\n\\v\\f\\r ]+|[\\t\\n\\v\\f\\r ]+$/g, '')\nL735: \nL736: if (!result.ref) {\nL737: \nL738: if (isWorkflowRepository) {\nL739: \nL740: result.ref = github.context.ref\nL741: \nL742: result.commit = github.context.sha\nL743: \nL744: // Some events have an unqualifed ref. For example when a PR is merged (pull_request closed event),\nL745: \nL746: // the ref is unqualifed like \"main\" instead of \"refs/heads/main\".\nL747: \nL748: if (result.commit && result.ref && !result.ref.startsWith('refs/')) {\nL749: \nL750: result.ref = `refs/heads/${result.ref}`\nL751: \nL752: }\nL753: \nL754: }\nL755: \nL756: }\nL757: \nL758: // SHA?\nL759: else if (asciiTrimmedRef.match(/^[0-9a-fA-F]{40}$/)) {\nL760: \nL761: result.commit = asciiTrimmedRef\nL762: \nL763: result.ref = ''\nL764: \nL765: }\nL766: \nL767: core.debug(`ref = '${result.ref}'`)\nL768: \nL769: core.debug(`commit = '${result.commit}'`)\nL770: \nL771: // Clean\nL772: \nL773: result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'\nL774: \nL775: core.debug(`clean = ${result.clean}`)\nL776: \nL777: // Filter\nL778: \nL779: const filter = core.getInput('filter')\nL780: \nL781: if (filter) {\nL782: \nL783: result.filter = filter\nL784: \nL785: }\nL786: \nL787: core.debug(`filter = ${result.filter}`)\nL788: \nL789: // Sparse checkout\nL790: const sparseCheckout = core.getMultilineInput('sparse-checkout')\nL791: \nL792: if (sparseCheckout.length) {\nL793: \nL794: result.sparseCheckout = sparseCheckout\nL795: \nL796: core.debug(`sparse checkout = ${result.sparseCheckout}`)\nL797: \nL798: }\nL799: \nL800: result.sparseCheckoutConeMode =\nL801: \nL802: (core.getInput('sparse-checkout-cone-mode') || 'true').toUpperCase() ===\nL803: \nL804: 'TRUE'\nL805: \nL806: // Fetch depth\nL807: \nL808: result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))\nL809: \nL810: if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {\nL811: \nL812: result.fetchDepth = 0\nL813: \nL814: }\nL815: core.debug(`fetch depth = ${result.fetchDepth}`)\nL816: \nL817: // Fetch tags\nL818: \nL819: result.fetchTags =\nL820: \nL821: (core.getInput('fetch-tags') || 'false').toUpperCase() === 'TRUE'\nL822: \nL823: core.debug(`fetch tags = ${result.fetchTags}`)\nL824: \nL825: // Show fetch progress\nL826: \nL827: result.showProgress =\nL828: \nL829: (core.getInput('show-progress') || 'true').toUpperCase() === 'TRUE'\nL830: \nL831: core.debug(`show progress = ${result.showProgress}`)\nL832: \nL833: // LFS\nL834: \nL835: result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'\nL836: \nL837: core.debug(`lfs = ${result.lfs}`)\nL838: \nL839: // Submodules\n"}}
{"i":1,"result":{"status":"fulfilled","value":{"chunk_id":"1693e0","wall_time_seconds":0.000002542,"exit_code":0,"original_token_count":755,"output":"name: Codex Review\n\n# Automated independent second-model review, formalized from\n# docs/production-hardening-backlog.md item #4 (\"Independent second-model\n# review as a standing practice\"). Runs Codex (a model with no stake in this\n# repo's prior conclusions) against every PR diff and posts its findings as a\n# PR comment. This job is advisory: it reports [P1]/[P2] findings and fails\n# the check on a [P1], but it does not itself block merge unless branch\n# protection is separately configured to require it — that's a deliberate,\n# explicit decision left to the repo owner, not made here.\n#\n# Requires ONE of:\n# - CODEX_AUTH_JSON: the contents of a `codex login`-produced ~/.codex/auth.json\n# (ChatGPT-subscription auth — ties usage to that account's plan, not a\n# separate API budget; see docs/production-hardening-backlog.md item #4)\n# - CODEX_API_KEY / OPENAI_API_KEY: a standard API key\n# Without one of these, this job reports that plainly and exits — it never\n# silently no-ops as if review happened. Secrets are only available to\n# `pull_request` runs triggered from THIS repo (never from a fork, by GitHub\n# Actions' own default security model — this workflow deliberately uses\n# `pull_request`, not `pull_request_target`, to keep that protection).\n#\n# KNOWN RESIDUAL RISK, not fully closed by this workflow: the reviewing\n# agent runs with read access to its own auth material while reviewing a\n# same-repo PR's diff, which could contain a prompt-injection attempt asking\n# it to reveal that material in its review output. This workflow redacts any\n# verbatim occurrence of the configured secret value from the posted comment\n# and job logs as a concrete backstop, but that is a partial mitigation, not\n# full isolation (a sufficiently reworded/re-encoded exfiltration attempt\n# could still evade a literal-substring redaction). Real isolation would mean\n# authenticating through a proxy that never hands the credential itself to\n# the process reading untrusted diff content — not implemented here. Treat\n# CODEX_AUTH_JSON as a real secret with this residual exposure in mind, not\n# as fully contained by this job.\n\non:\n pull_request:\n types: [opened, synchronize, reopened]\n branches: [main]\n\npermissions:\n contents: read\n pull-requests: write\n\nconcurrency:\n group: codex-review-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n\njobs:\n codex-review:\n name: Codex independent review\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n # persist-credentials: false — this job's own GITHUB_TOKEN\n # (pull-requests: write) must not be left readable by the same agent\n # that reads untrusted PR diff content; this job never needs to push.\n - name: Checkout\n uses: actions/checkout@v6\n with:\n fetch-depth: 0\n persist-credentials: false\n\n # Secrets are passed via env:, never interpolated with ${{ }} directly\n # into the script body — GitHub Actions writes a run: step's script to\n"}}}
{"i":2,"result":{"status":"fulfilled","value":{"chunk_id":"5edf9a","wall_time_seconds":0.000001625,"exit_code":0,"original_token_count":1087,"output":"scripts/analyze-latest-review.py:2:\"\"\"Decide, from evidence, whether a just-completed review round newly\nscripts/analyze-latest-review.py:5:This is the piece that closes the gap named while building this archive:\nscripts/analyze-latest-review.py:7:mechanism recommendation from the archive's accumulated data, but something\nscripts/analyze-latest-review.py:12:recommendation with vs. without the latest round's findings included, and\nscripts/analyze-latest-review.py:14:\"mechanism\" (or newly appears at/above threshold) because of this round\nscripts/analyze-latest-review.py:16:rounds, which would otherwise fire on every single subsequent round\nscripts/analyze-latest-review.py:19:have read the archive and noticed the pattern themselves.\nscripts/analyze-latest-review.py:22: python3 analyze-latest-review.py <archive.jsonl> <review-comment.txt> [--threshold N]\nscripts/analyze-latest-review.py:51:def load_archive(path: str) -> list[dict]:\nscripts/analyze-latest-review.py:61:def next_round_number(entries: list[dict]) -> int:\nscripts/analyze-latest-review.py:62: rounds = [e.get(\"round\", 0) for e in entries]\nscripts/analyze-latest-review.py:63: return (max(rounds) + 1) if rounds else 1\nscripts/analyze-latest-review.py:72: archive_entries: list[dict], new_findings: list[str], threshold: int\nscripts/analyze-latest-review.py:74: \"\"\"Compare recommendations with vs. without the new round's findings.\nscripts/analyze-latest-review.py:77: round is included -- i.e. this round is the one that tipped it over,\nscripts/analyze-latest-review.py:78: not a topic that already exceeded the threshold in prior rounds.\nscripts/analyze-latest-review.py:80: before = recommendations_by_topic(archive_entries, threshold)\nscripts/analyze-latest-review.py:82: new_round_entry = {\"round\": next_round_number(archive_entries), \"findings\": new_findings}\nscripts/analyze-latest-review.py:83: after_entries = [*archive_entries, new_round_entry]\nscripts/analyze-latest-review.py:99: parser.add_argument(\"archive_path\")\nscripts/analyze-latest-review.py:104: archive_entries = load_archive(args.archive_path)\nscripts/analyze-latest-review.py:116: newly_crossed = find_newly_crossed_topics(archive_entries, new_findings, args.threshold)\nscripts/analyze-latest-review.py:122: f\"as of this round's findings.\"\nscripts/analyze-latest-review.py:125: print(\"No topic newly crosses the mechanism-fix threshold this round.\")\n.github/workflows/codex-review.yml:51: name: Codex independent review\n.github/workflows/codex-review.yml:299: await github.rest.issues.createComment({\n.github/workflows/codex-review.yml:303: body: `### Codex independent review\\n\\n${body}\\n\\n---\\n*Generated by an independent model reviewing this diff with no stake in prior conclusions on it — see [docs/production-hardening-backlog.md](../blob/main/docs/production-hardening-backlog.md) item #4.*`,\n.github/workflows/archive-and-recommend.yml:6:# docs/self-improvement-archive.jsonl's accumulated evidence, and\n.github/workflows/archive-and-recommend.yml:7:# scripts/analyze-latest-review.py could tell whether the LATEST round is\n.github/workflows/archive-and-recommend.yml:11:# archive. This workflow makes it automatic, but ONLY for a deliberately\n.github/workflows/archive-and-recommend.yml:21:# it reads (the posted review comment, the archive file) is already public,\n.github/workflows/archive-and-recommend.yml:34: group: archive-and-recommend-${{ github.event.workflow_run.id }}\n.github/workflows/archive-and-recommend.yml:66: (c.body || \"\").startsWith(\"### Codex independent review\")\n.github/workflows/archive-and-recommend.yml:83: docs/self-improvement-archive.jsonl \\\n.github/workflows/archive-and-recommend.yml:124: `Automatically opened by [\\`archive-and-recommend.yml\\`](../blob/main/.github/workflows/archive-and-recommend.yml).`,\n.github/workflows/archive-and-recommend.yml:127: `threshold in \\`docs/self-improvement-archive.jsonl\\`, as of the`,\n.github/workflows/archive-and-recommend.yml:133: `Run \\`python3 scripts/detect-recurring-pattern.py docs/self-improvement-archive.jsonl\\``,\n.github/workflows/archive-and-recommend.yml:134: `for the full evidence trail (which rounds, which findings) behind this`,\n"}}}
{"i":3,"result":{"status":"fulfilled","value":{"chunk_id":"f360e7","wall_time_seconds":0.000001458,"exit_code":1,"original_token_count":0,"output":""}}}>sed -n '1,65p' .github/workflows/codex-review.yml{"i":0,"result":{"status":"fulfilled","value":"Events that trigger workflows - GitHub Docs (https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows)\nciteturn1view0 [wordlim: 200] Crawled: today; Content type: text/html; Source: find({\"ref_id\":\"turn0view0\",\"pattern\":\"## workflow_run\"}); Total lines: 1321\nL1177: If you run this workflow from a browser you must enter values for the required inputs manually before the workflow will run.\nL1178: \nL1179: cite161†Image: Screenshot of a list of workflow runs. A dropdown menu, labeled \"Run workflow\" and expanded to show input fields, is outlined in dark orange. L1180: \nL1181: You can also pass inputs when you run a workflow from a script, or by using GitHub CLI. For example:\nL1182: \nL1183: `gh workflow run run-tests.yml -f logLevel=warning -f tags=false -f environment=staging\nL1184: `\nL1185: For more information, see the GitHub CLI information in cite158†Manually running a workflow .\nL1186: ## cite39†`workflow_run` L1187: \nL1188: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL1189: --- | --- | --- | ---\nL1190: cite162†`workflow_run` | - `completed`\nL1191: - `requested`\nL1192: - `in_progress` | Last commit on default branch | Default branch\nL1193: \nL1194: Note\nL1195: * More than one activity type triggers this event. The `requested` activity type does not occur when a workflow is re-run. For information about each activity type, see cite162†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL1196: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL1197: * You can't use `workflow_run` to chain together more than three levels of workflows. For example, if you attempt to trigger five workflows (named `B` to `F`) to run sequentially after an initial workflow `A` has run (that is: `A` → `B` → `C` → `D` → `E` → `F`), workflows `E` and `F` will not be run.\nL1198: This event occurs when a workflow run is requested or completed. It allows you to execute a workflow based on execution or completion of another workflow. The workflow started by the `workflow_run` event is able to access secrets and write tokens, even if the previous workflow was not. This is useful in cases where the previous workflow is intentionally not privileged, but you need to take a privileged action in a later workflow.\nL1199: \nL1200: Warning\nL1201: Running untrusted code on the `workflow_run` trigger may lead to security vulnerabilities. These vulnerabilities include cache poisoning and granting unintended access to write privileges or secrets. For more information, see cite123†Secure use reference in the GitHub Enterprise Cloud documentation, and cite124†Preventing pwn requests†securitylab.github.com on the GitHub Security Lab website.\nL1202: In this example, a workflow is configured to run after the separate \"Run Tests\" workflow completes.\nL1203: \nL1204: `on:\nL1205: workflow_run:\nL1206: workflows: [Run Tests]\nL1207: types:\nL1208: - completed\nL1209: `\nL1210: If you specify multiple `workflows` for the `workflow_run` event, only one of the workflows needs to run. For example, a workflow with the following trigger will run whenever the \"Staging\" workflow or the \"Lab\" workflow completes.\nL1211: \nL1212: `on:\nL1213: workflow_run:\nL1214: workflows: [Staging, Lab]\nL1215: types:\nL1216: - completed\nL1217: `\nL1218: ### cite163†Running a workflow based on the conclusion of another workflow L1219: A workflow run is triggered regardless of the conclusion of the previous workflow. If you want to run a job or step based on the result of the triggering workflow, you can use a conditional with the `github.event.workflow_run.conclusion` property. For example, this workflow will run whenever a workflow named \"Build\" completes, but the `on-success` job will only run if the \"Build\" workflow succeeded, and the `on-failure` job will only run if the \"Build\" workflow failed:\nL1220: \nL1221: `on:\nL1222: workflow_run:\nL1223: workflows: [Build]\nL1224: types: [completed]\nL1225: \nL1226: jobs:\nL1227: on-success:\nL1228: runs-on: ubuntu-latest\nL1229: if: ${{ github.event.workflow_run.conclusion == 'success' }}\nL1230: steps:\nL1231: - run: echo 'The triggering workflow passed'\nL1232: on-failure:\nL1233: runs-on: ubuntu-latest\nL1234: if: ${{ github.event.workflow_run.conclusion == 'failure' }}\nL1235: steps:\nL1236: - run: echo 'The triggering workflow failed'\nL1237: `\nL1238: ### cite164†Limiting your workflow to run based on branches L1239: You can use the `branches` or `branches-ignore` filter to specify what branches the triggering workflow must run on in order to trigger your workflow. For more information, see cite165†Workflow syntax for GitHub Actions . For example, a workflow with the following trigger will only run when the workflow named `Build` runs on a branch named `canary`.\nL1240: \nL1241: `on:\nL1242: workflow_run:\nL1243: workflows: [Build]\nL1244: types: [requested]\nL1245: branches: [canary]\nL1246: `\nL1247: ### cite166†Using data from the triggering workflow L1248: \nL1249: You can access the cite162†`workflow_run` event payload that corresponds to the workflow that triggered your workflow. For example, if your triggering workflow generates artifacts, a workflow triggered with the `workflow_run` event can access these artifacts.\nL1250: The following workflow uploads data as an artifact. (In this simplified example, the data is the pull request number.)\nL1251: \nL1252: `name: Upload data\nL1253: \nL1254: on:\nL1255: pull_request:\nL1256: \nL1257: jobs:\nL1258: upload:\nL1259: runs-on: ubuntu-latest\nL1260: \nL1261: steps:\nL1262: - name: Save PR number\nL1263: env:\nL1264: PR_NUMBER: ${{ github.event.number }}\nL1265: run: |\nL1266: mkdir -p ./pr\nL1267: echo $PR_NUMBER > ./pr/pr_number\nL1268: - uses: actions/upload-artifact@v4\nL1269: with:\nL1270: name: pr_number\nL1271: path: pr/\nL1272: `\nL1273: When a run of the above workflow completes, it triggers a run of the following workflow. The following workflow uses the `github.event.workflow_run` context and the actions/download-artifact@v5 action to download the artifact that was uploaded by the above workflow, then comments on the pull request whose number was uploaded as an artifact.\nL1274: \nL1275: `name: Use the data\nL1276: \nL1277: on:\nL1278: workflow_run:\nL1279: workflows: [Upload data]\nL1280: types:\nL1281: - completed\nL1282: \nL1283: jobs:\nL1284: download:\nL1285: runs-on: ubuntu-latest\nL1286: permissions:\nL1287: actions: read\nL1288: issues: write\nL1289: steps:\nL1290: - name: 'Download artifact'\nL1291: uses: actions/download-artifact@v5\nL1292: with:\nL1293: name: pr_number\nL1294: # do not extract in the workspace dir that may contain executable scripts\nL1295: path: ${{ runner.temp }}/artifacts\nL1296: run-id: ${{ github.event.workflow_run.id }}\nL1297: github-token: ${{ secrets.GITHUB_TOKEN }}\nL1298: - name: 'Comment on PR'\nL1299: uses: actions/github-script@v8\nL1300: with:\nL1301: github-token: ${{ secrets.GITHUB_TOKEN }}\nL1302: script: |\nL1303: const fs = require('fs');\nL1304: const path = require('path');\nL1305: const temp = '${{ runner.temp }}/artifacts';\nL1306: const issue_number_raw = fs.readFileSync(path.join(temp, 'pr_number'), 'utf8').trim();\nL1307: const issue_number = Number(issue_number_raw);\nL1308: if (!Number.isInteger(issue_number)) {\nL1309: throw new Error(`Invalid PR number in pr_number artifact: \"${issue_number_raw}\"`);\nL1310: }\nL1311: await github.rest.issues.createComment({\nL1312: owner: context.repo.owner,\nL1313: repo: context.repo.repo,\nL1314: issue_number: issue_number,\nL1315: body: 'Thank you for the PR!'\nL1316: });\n--------------------------------------------------------------------------------\ncheckout/src/input-helper.ts at v4 · actions/checkout · GitHub (https://github.com/actions/checkout/blob/v4/src/input-helper.ts)\nciteturn1view1 [wordlim: 200] Crawled: today; Content type: text/html; Source: find({\"ref_id\":\"turn0view1\",\"pattern\":\"result.ref = github.context.ref\"}); Total lines: 939\nL636: `${github.context.repo.owner}/${github.context.repo.repo}`\nL637: \nL638: core.debug(`qualified repository = '${qualifiedRepository}'`)\nL639: \nL640: const splitRepository = qualifiedRepository.split('/')\nL641: \nL642: if (\nL643: \nL644: splitRepository.length !== 2 ||\nL645: \nL646: !splitRepository[0] ||\nL647: \nL648: !splitRepository[1]\nL649: \nL650: ) {\nL651: \nL652: throw new Error(\nL653: \nL654: `Invalid repository '${qualifiedRepository}'. Expected format {owner}/{repo}.`\nL655: \nL656: )\nL657: \nL658: }\nL659: \nL660: result.repositoryOwner = splitRepository[0]\nL661: \nL662: result.repositoryName = splitRepository[1]\nL663: \nL664: // Repository path\nL665: result.repositoryPath = core.getInput('path') || '.'\nL666: \nL667: result.repositoryPath = path.resolve(\nL668: \nL669: githubWorkspacePath,\nL670: \nL671: result.repositoryPath\nL672: \nL673: )\nL674: \nL675: if (\nL676: \nL677: !(result.repositoryPath + path.sep).startsWith(\nL678: \nL679: githubWorkspacePath + path.sep\nL680: \nL681: )\nL682: \nL683: ) {\nL684: \nL685: throw new Error(\nL686: \nL687: `Repository path '${result.repositoryPath}' is not under '${githubWorkspacePath}'`\nL688: \nL689: )\nL690: \nL691: }\nL692: \nL693: // Workflow repository?\nL694: \nL695: const isWorkflowRepository =\nL696: \nL697: qualifiedRepository.toUpperCase() ===\nL698: \nL699: `${github.context.repo.owner}/${github.context.repo.repo}`.toUpperCase()\nL700: // Source branch, source version\nL701: \nL702: result.ref = core.getInput('ref')\nL703: \nL704: // core.getInput()'s default trim strips a range of Unicode characters such as a\nL705: \nL706: // leading BOM (U+FEFF) or NBSP (U+00A0). Those are valid in a git ref name, so\nL707: \nL708: // a fork branch named \"<BOM>\" + 40 hex chars would trim down to a bare SHA and\nL709: \nL710: // be silently reclassified as a commit, bypassing the unsafe fork PR checkout\nL711: \nL712: // guard.\nL713: \nL714: //\nL715: \nL716: // The trim below strips only the ASCII whitespace characters which are all forbidden\nL717: // in a git branch name.\nL718: \nL719: // \\t U+0009 horizontal tab - ASCII control, forbidden in ref names\nL720: \nL721: // \\n U+000A line feed - ASCII control, forbidden in ref names\nL722: \nL723: // \\v U+000B vertical tab - ASCII control, forbidden in ref names\nL724: \nL725: // \\f U+000C form feed - ASCII control, forbidden in ref names\nL726: \nL727: // \\r U+000D carriage return - ASCII control, forbidden in ref names\nL728: \nL729: // ' ' U+0020 space - forbidden in ref names\nL730: \nL731: const asciiTrimmedRef = core\nL732: \nL733: .getInput('ref', {trimWhitespace: false})\nL734: .replace(/^[\\t\\n\\v\\f\\r ]+|[\\t\\n\\v\\f\\r ]+$/g, '')\nL735: \nL736: if (!result.ref) {\nL737: \nL738: if (isWorkflowRepository) {\nL739: \nL740: result.ref = github.context.ref\nL741: \nL742: result.commit = github.context.sha\nL743: \nL744: // Some events have an unqualifed ref. For example when a PR is merged (pull_request closed event),\nL745: \nL746: // the ref is unqualifed like \"main\" instead of \"refs/heads/main\".\nL747: \nL748: if (result.commit && result.ref && !result.ref.startsWith('refs/')) {\nL749: \nL750: result.ref = `refs/heads/${result.ref}`\nL751: \nL752: }\nL753: \nL754: }\nL755: \nL756: }\nL757: \nL758: // SHA?\nL759: else if (asciiTrimmedRef.match(/^[0-9a-fA-F]{40}$/)) {\nL760: \nL761: result.commit = asciiTrimmedRef\nL762: \nL763: result.ref = ''\nL764: \nL765: }\nL766: \nL767: core.debug(`ref = '${result.ref}'`)\nL768: \nL769: core.debug(`commit = '${result.commit}'`)\nL770: \nL771: // Clean\nL772: \nL773: result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'\nL774: \nL775: core.debug(`clean = ${result.clean}`)\nL776: \nL777: // Filter\nL778: \nL779: const filter = core.getInput('filter')\nL780: \nL781: if (filter) {\nL782: \nL783: result.filter = filter\nL784: \nL785: }\nL786: \nL787: core.debug(`filter = ${result.filter}`)\nL788: \nL789: // Sparse checkout\nL790: const sparseCheckout = core.getMultilineInput('sparse-checkout')\nL791: \nL792: if (sparseCheckout.length) {\nL793: \nL794: result.sparseCheckout = sparseCheckout\nL795: \nL796: core.debug(`sparse checkout = ${result.sparseCheckout}`)\nL797: \nL798: }\nL799: \nL800: result.sparseCheckoutConeMode =\nL801: \nL802: (core.getInput('sparse-checkout-cone-mode') || 'true').toUpperCase() ===\nL803: \nL804: 'TRUE'\nL805: \nL806: // Fetch depth\nL807: \nL808: result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))\nL809: \nL810: if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {\nL811: \nL812: result.fetchDepth = 0\nL813: \nL814: }\nL815: core.debug(`fetch depth = ${result.fetchDepth}`)\nL816: \nL817: // Fetch tags\nL818: \nL819: result.fetchTags =\nL820: \nL821: (core.getInput('fetch-tags') || 'false').toUpperCase() === 'TRUE'\nL822: \nL823: core.debug(`fetch tags = ${result.fetchTags}`)\nL824: \nL825: // Show fetch progress\nL826: \nL827: result.showProgress =\nL828: \nL829: (core.getInput('show-progress') || 'true').toUpperCase() === 'TRUE'\nL830: \nL831: core.debug(`show progress = ${result.showProgress}`)\nL832: \nL833: // LFS\nL834: \nL835: result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'\nL836: \nL837: core.debug(`lfs = ${result.lfs}`)\nL838: \nL839: // Submodules\n"}}
{"i":1,"result":{"status":"fulfilled","value":{"chunk_id":"1693e0","wall_time_seconds":0.000002542,"exit_code":0,"original_token_count":755,"output":"name: Codex Review\n\n# Automated independent second-model review, formalized from\n# docs/production-hardening-backlog.md item #4 (\"Independent second-model\n# review as a standing practice\"). Runs Codex (a model with no stake in this\n# repo's prior conclusions) against every PR diff and posts its findings as a\n# PR comment. This job is advisory: it reports [P1]/[P2] findings and fails\n# the check on a [P1], but it does not itself block merge unless branch\n# protection is separately configured to require it — that's a deliberate,\n# explicit decision left to the repo owner, not made here.\n#\n# Requires ONE of:\n# - CODEX_AUTH_JSON: the contents of a `codex login`-produced ~/.codex/auth.json\n# (ChatGPT-subscription auth — ties usage to that account's plan, not a\n# separate API budget; see docs/production-hardening-backlog.md item #4)\n# - CODEX_API_KEY / OPENAI_API_KEY: a standard API key\n# Without one of these, this job reports that plainly and exits — it never\n# silently no-ops as if review happened. Secrets are only available to\n# `pull_request` runs triggered from THIS repo (never from a fork, by GitHub\n# Actions' own default security model — this workflow deliberately uses\n# `pull_request`, not `pull_request_target`, to keep that protection).\n#\n# KNOWN RESIDUAL RISK, not fully closed by this workflow: the reviewing\n# agent runs with read access to its own auth material while reviewing a\n# same-repo PR's diff, which could contain a prompt-injection attempt asking\n# it to reveal that material in its review output. This workflow redacts any\n# verbatim occurrence of the configured secret value from the posted comment\n# and job logs as a concrete backstop, but that is a partial mitigation, not\n# full isolation (a sufficiently reworded/re-encoded exfiltration attempt\n# could still evade a literal-substring redaction). Real isolation would mean\n# authenticating through a proxy that never hands the credential itself to\n# the process reading untrusted diff content — not implemented here. Treat\n# CODEX_AUTH_JSON as a real secret with this residual exposure in mind, not\n# as fully contained by this job.\n\non:\n pull_request:\n types: [opened, synchronize, reopened]\n branches: [main]\n\npermissions:\n contents: read\n pull-requests: write\n\nconcurrency:\n group: codex-review-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n\njobs:\n codex-review:\n name: Codex independent review\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n # persist-credentials: false — this job's own GITHUB_TOKEN\n # (pull-requests: write) must not be left readable by the same agent\n # that reads untrusted PR diff content; this job never needs to push.\n - name: Checkout\n uses: actions/checkout@v6\n with:\n fetch-depth: 0\n persist-credentials: false\n\n # Secrets are passed via env:, never interpolated with ${{ }} directly\n # into the script body — GitHub Actions writes a run: step's script to\n"}}}
{"i":2,"result":{"status":"fulfilled","value":{"chunk_id":"5edf9a","wall_time_seconds":0.000001625,"exit_code":0,"original_token_count":1087,"output":"scripts/analyze-latest-review.py:2:\"\"\"Decide, from evidence, whether a just-completed review round newly\nscripts/analyze-latest-review.py:5:This is the piece that closes the gap named while building this archive:\nscripts/analyze-latest-review.py:7:mechanism recommendation from the archive's accumulated data, but something\nscripts/analyze-latest-review.py:12:recommendation with vs. without the latest round's findings included, and\nscripts/analyze-latest-review.py:14:\"mechanism\" (or newly appears at/above threshold) because of this round\nscripts/analyze-latest-review.py:16:rounds, which would otherwise fire on every single subsequent round\nscripts/analyze-latest-review.py:19:have read the archive and noticed the pattern themselves.\nscripts/analyze-latest-review.py:22: python3 analyze-latest-review.py <archive.jsonl> <review-comment.txt> [--threshold N]\nscripts/analyze-latest-review.py:51:def load_archive(path: str) -> list[dict]:\nscripts/analyze-latest-review.py:61:def next_round_number(entries: list[dict]) -> int:\nscripts/analyze-latest-review.py:62: rounds = [e.get(\"round\", 0) for e in entries]\nscripts/analyze-latest-review.py:63: return (max(rounds) + 1) if rounds else 1\nscripts/analyze-latest-review.py:72: archive_entries: list[dict], new_findings: list[str], threshold: int\nscripts/analyze-latest-review.py:74: \"\"\"Compare recommendations with vs. without the new round's findings.\nscripts/analyze-latest-review.py:77: round is included -- i.e. this round is the one that tipped it over,\nscripts/analyze-latest-review.py:78: not a topic that already exceeded the threshold in prior rounds.\nscripts/analyze-latest-review.py:80: before = recommendations_by_topic(archive_entries, threshold)\nscripts/analyze-latest-review.py:82: new_round_entry = {\"round\": next_round_number(archive_entries), \"findings\": new_findings}\nscripts/analyze-latest-review.py:83: after_entries = [*archive_entries, new_round_entry]\nscripts/analyze-latest-review.py:99: parser.add_argument(\"archive_path\")\nscripts/analyze-latest-review.py:104: archive_entries = load_archive(args.archive_path)\nscripts/analyze-latest-review.py:116: newly_crossed = find_newly_crossed_topics(archive_entries, new_findings, args.threshold)\nscripts/analyze-latest-review.py:122: f\"as of this round's findings.\"\nscripts/analyze-latest-review.py:125: print(\"No topic newly crosses the mechanism-fix threshold this round.\")\n.github/workflows/codex-review.yml:51: name: Codex independent review\n.github/workflows/codex-review.yml:299: await github.rest.issues.createComment({\n.github/workflows/codex-review.yml:303: body: `### Codex independent review\\n\\n${body}\\n\\n---\\n*Generated by an independent model reviewing this diff with no stake in prior conclusions on it — see [docs/production-hardening-backlog.md](../blob/main/docs/production-hardening-backlog.md) item #4.*`,\n.github/workflows/archive-and-recommend.yml:6:# docs/self-improvement-archive.jsonl's accumulated evidence, and\n.github/workflows/archive-and-recommend.yml:7:# scripts/analyze-latest-review.py could tell whether the LATEST round is\n.github/workflows/archive-and-recommend.yml:11:# archive. This workflow makes it automatic, but ONLY for a deliberately\n.github/workflows/archive-and-recommend.yml:21:# it reads (the posted review comment, the archive file) is already public,\n.github/workflows/archive-and-recommend.yml:34: group: archive-and-recommend-${{ github.event.workflow_run.id }}\n.github/workflows/archive-and-recommend.yml:66: (c.body || \"\").startsWith(\"### Codex independent review\")\n.github/workflows/archive-and-recommend.yml:83: docs/self-improvement-archive.jsonl \\\n.github/workflows/archive-and-recommend.yml:124: `Automatically opened by [\\`archive-and-recommend.yml\\`](../blob/main/.github/workflows/archive-and-recommend.yml).`,\n.github/workflows/archive-and-recommend.yml:127: `threshold in \\`docs/self-improvement-archive.jsonl\\`, as of the`,\n.github/workflows/archive-and-recommend.yml:133: `Run \\`python3 scripts/detect-recurring-pattern.py docs/self-improvement-archive.jsonl\\``,\n.github/workflows/archive-and-recommend.yml:134: `for the full evidence trail (which rounds, which findings) behind this`,\n"}}}
{"i":3,"result":{"status":"fulfilled","value":{"chunk_id":"f360e7","wall_time_seconds":0.000001458,"exit_code":1,"original_token_count":0,"output":""}}}>rg -n 'archive|round|createComment|updateComment|independent review|head.sha' .github/workflows/codex-review.yml .github/workflows/archive-and-recommend.yml scripts/analyze-latest-review.py{"i":0,"result":{"status":"fulfilled","value":"Events that trigger workflows - GitHub Docs (https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows)\nciteturn1view0 [wordlim: 200] Crawled: today; Content type: text/html; Source: find({\"ref_id\":\"turn0view0\",\"pattern\":\"## workflow_run\"}); Total lines: 1321\nL1177: If you run this workflow from a browser you must enter values for the required inputs manually before the workflow will run.\nL1178: \nL1179: cite161†Image: Screenshot of a list of workflow runs. A dropdown menu, labeled \"Run workflow\" and expanded to show input fields, is outlined in dark orange. L1180: \nL1181: You can also pass inputs when you run a workflow from a script, or by using GitHub CLI. For example:\nL1182: \nL1183: `gh workflow run run-tests.yml -f logLevel=warning -f tags=false -f environment=staging\nL1184: `\nL1185: For more information, see the GitHub CLI information in cite158†Manually running a workflow .\nL1186: ## cite39†`workflow_run` L1187: \nL1188: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL1189: --- | --- | --- | ---\nL1190: cite162†`workflow_run` | - `completed`\nL1191: - `requested`\nL1192: - `in_progress` | Last commit on default branch | Default branch\nL1193: \nL1194: Note\nL1195: * More than one activity type triggers this event. The `requested` activity type does not occur when a workflow is re-run. For information about each activity type, see cite162†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL1196: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL1197: * You can't use `workflow_run` to chain together more than three levels of workflows. For example, if you attempt to trigger five workflows (named `B` to `F`) to run sequentially after an initial workflow `A` has run (that is: `A` → `B` → `C` → `D` → `E` → `F`), workflows `E` and `F` will not be run.\nL1198: This event occurs when a workflow run is requested or completed. It allows you to execute a workflow based on execution or completion of another workflow. The workflow started by the `workflow_run` event is able to access secrets and write tokens, even if the previous workflow was not. This is useful in cases where the previous workflow is intentionally not privileged, but you need to take a privileged action in a later workflow.\nL1199: \nL1200: Warning\nL1201: Running untrusted code on the `workflow_run` trigger may lead to security vulnerabilities. These vulnerabilities include cache poisoning and granting unintended access to write privileges or secrets. For more information, see cite123†Secure use reference in the GitHub Enterprise Cloud documentation, and cite124†Preventing pwn requests†securitylab.github.com on the GitHub Security Lab website.\nL1202: In this example, a workflow is configured to run after the separate \"Run Tests\" workflow completes.\nL1203: \nL1204: `on:\nL1205: workflow_run:\nL1206: workflows: [Run Tests]\nL1207: types:\nL1208: - completed\nL1209: `\nL1210: If you specify multiple `workflows` for the `workflow_run` event, only one of the workflows needs to run. For example, a workflow with the following trigger will run whenever the \"Staging\" workflow or the \"Lab\" workflow completes.\nL1211: \nL1212: `on:\nL1213: workflow_run:\nL1214: workflows: [Staging, Lab]\nL1215: types:\nL1216: - completed\nL1217: `\nL1218: ### cite163†Running a workflow based on the conclusion of another workflow L1219: A workflow run is triggered regardless of the conclusion of the previous workflow. If you want to run a job or step based on the result of the triggering workflow, you can use a conditional with the `github.event.workflow_run.conclusion` property. For example, this workflow will run whenever a workflow named \"Build\" completes, but the `on-success` job will only run if the \"Build\" workflow succeeded, and the `on-failure` job will only run if the \"Build\" workflow failed:\nL1220: \nL1221: `on:\nL1222: workflow_run:\nL1223: workflows: [Build]\nL1224: types: [completed]\nL1225: \nL1226: jobs:\nL1227: on-success:\nL1228: runs-on: ubuntu-latest\nL1229: if: ${{ github.event.workflow_run.conclusion == 'success' }}\nL1230: steps:\nL1231: - run: echo 'The triggering workflow passed'\nL1232: on-failure:\nL1233: runs-on: ubuntu-latest\nL1234: if: ${{ github.event.workflow_run.conclusion == 'failure' }}\nL1235: steps:\nL1236: - run: echo 'The triggering workflow failed'\nL1237: `\nL1238: ### cite164†Limiting your workflow to run based on branches L1239: You can use the `branches` or `branches-ignore` filter to specify what branches the triggering workflow must run on in order to trigger your workflow. For more information, see cite165†Workflow syntax for GitHub Actions . For example, a workflow with the following trigger will only run when the workflow named `Build` runs on a branch named `canary`.\nL1240: \nL1241: `on:\nL1242: workflow_run:\nL1243: workflows: [Build]\nL1244: types: [requested]\nL1245: branches: [canary]\nL1246: `\nL1247: ### cite166†Using data from the triggering workflow L1248: \nL1249: You can access the cite162†`workflow_run` event payload that corresponds to the workflow that triggered your workflow. For example, if your triggering workflow generates artifacts, a workflow triggered with the `workflow_run` event can access these artifacts.\nL1250: The following workflow uploads data as an artifact. (In this simplified example, the data is the pull request number.)\nL1251: \nL1252: `name: Upload data\nL1253: \nL1254: on:\nL1255: pull_request:\nL1256: \nL1257: jobs:\nL1258: upload:\nL1259: runs-on: ubuntu-latest\nL1260: \nL1261: steps:\nL1262: - name: Save PR number\nL1263: env:\nL1264: PR_NUMBER: ${{ github.event.number }}\nL1265: run: |\nL1266: mkdir -p ./pr\nL1267: echo $PR_NUMBER > ./pr/pr_number\nL1268: - uses: actions/upload-artifact@v4\nL1269: with:\nL1270: name: pr_number\nL1271: path: pr/\nL1272: `\nL1273: When a run of the above workflow completes, it triggers a run of the following workflow. The following workflow uses the `github.event.workflow_run` context and the actions/download-artifact@v5 action to download the artifact that was uploaded by the above workflow, then comments on the pull request whose number was uploaded as an artifact.\nL1274: \nL1275: `name: Use the data\nL1276: \nL1277: on:\nL1278: workflow_run:\nL1279: workflows: [Upload data]\nL1280: types:\nL1281: - completed\nL1282: \nL1283: jobs:\nL1284: download:\nL1285: runs-on: ubuntu-latest\nL1286: permissions:\nL1287: actions: read\nL1288: issues: write\nL1289: steps:\nL1290: - name: 'Download artifact'\nL1291: uses: actions/download-artifact@v5\nL1292: with:\nL1293: name: pr_number\nL1294: # do not extract in the workspace dir that may contain executable scripts\nL1295: path: ${{ runner.temp }}/artifacts\nL1296: run-id: ${{ github.event.workflow_run.id }}\nL1297: github-token: ${{ secrets.GITHUB_TOKEN }}\nL1298: - name: 'Comment on PR'\nL1299: uses: actions/github-script@v8\nL1300: with:\nL1301: github-token: ${{ secrets.GITHUB_TOKEN }}\nL1302: script: |\nL1303: const fs = require('fs');\nL1304: const path = require('path');\nL1305: const temp = '${{ runner.temp }}/artifacts';\nL1306: const issue_number_raw = fs.readFileSync(path.join(temp, 'pr_number'), 'utf8').trim();\nL1307: const issue_number = Number(issue_number_raw);\nL1308: if (!Number.isInteger(issue_number)) {\nL1309: throw new Error(`Invalid PR number in pr_number artifact: \"${issue_number_raw}\"`);\nL1310: }\nL1311: await github.rest.issues.createComment({\nL1312: owner: context.repo.owner,\nL1313: repo: context.repo.repo,\nL1314: issue_number: issue_number,\nL1315: body: 'Thank you for the PR!'\nL1316: });\n--------------------------------------------------------------------------------\ncheckout/src/input-helper.ts at v4 · actions/checkout · GitHub (https://github.com/actions/checkout/blob/v4/src/input-helper.ts)\nciteturn1view1 [wordlim: 200] Crawled: today; Content type: text/html; Source: find({\"ref_id\":\"turn0view1\",\"pattern\":\"result.ref = github.context.ref\"}); Total lines: 939\nL636: `${github.context.repo.owner}/${github.context.repo.repo}`\nL637: \nL638: core.debug(`qualified repository = '${qualifiedRepository}'`)\nL639: \nL640: const splitRepository = qualifiedRepository.split('/')\nL641: \nL642: if (\nL643: \nL644: splitRepository.length !== 2 ||\nL645: \nL646: !splitRepository[0] ||\nL647: \nL648: !splitRepository[1]\nL649: \nL650: ) {\nL651: \nL652: throw new Error(\nL653: \nL654: `Invalid repository '${qualifiedRepository}'. Expected format {owner}/{repo}.`\nL655: \nL656: )\nL657: \nL658: }\nL659: \nL660: result.repositoryOwner = splitRepository[0]\nL661: \nL662: result.repositoryName = splitRepository[1]\nL663: \nL664: // Repository path\nL665: result.repositoryPath = core.getInput('path') || '.'\nL666: \nL667: result.repositoryPath = path.resolve(\nL668: \nL669: githubWorkspacePath,\nL670: \nL671: result.repositoryPath\nL672: \nL673: )\nL674: \nL675: if (\nL676: \nL677: !(result.repositoryPath + path.sep).startsWith(\nL678: \nL679: githubWorkspacePath + path.sep\nL680: \nL681: )\nL682: \nL683: ) {\nL684: \nL685: throw new Error(\nL686: \nL687: `Repository path '${result.repositoryPath}' is not under '${githubWorkspacePath}'`\nL688: \nL689: )\nL690: \nL691: }\nL692: \nL693: // Workflow repository?\nL694: \nL695: const isWorkflowRepository =\nL696: \nL697: qualifiedRepository.toUpperCase() ===\nL698: \nL699: `${github.context.repo.owner}/${github.context.repo.repo}`.toUpperCase()\nL700: // Source branch, source version\nL701: \nL702: result.ref = core.getInput('ref')\nL703: \nL704: // core.getInput()'s default trim strips a range of Unicode characters such as a\nL705: \nL706: // leading BOM (U+FEFF) or NBSP (U+00A0). Those are valid in a git ref name, so\nL707: \nL708: // a fork branch named \"<BOM>\" + 40 hex chars would trim down to a bare SHA and\nL709: \nL710: // be silently reclassified as a commit, bypassing the unsafe fork PR checkout\nL711: \nL712: // guard.\nL713: \nL714: //\nL715: \nL716: // The trim below strips only the ASCII whitespace characters which are all forbidden\nL717: // in a git branch name.\nL718: \nL719: // \\t U+0009 horizontal tab - ASCII control, forbidden in ref names\nL720: \nL721: // \\n U+000A line feed - ASCII control, forbidden in ref names\nL722: \nL723: // \\v U+000B vertical tab - ASCII control, forbidden in ref names\nL724: \nL725: // \\f U+000C form feed - ASCII control, forbidden in ref names\nL726: \nL727: // \\r U+000D carriage return - ASCII control, forbidden in ref names\nL728: \nL729: // ' ' U+0020 space - forbidden in ref names\nL730: \nL731: const asciiTrimmedRef = core\nL732: \nL733: .getInput('ref', {trimWhitespace: false})\nL734: .replace(/^[\\t\\n\\v\\f\\r ]+|[\\t\\n\\v\\f\\r ]+$/g, '')\nL735: \nL736: if (!result.ref) {\nL737: \nL738: if (isWorkflowRepository) {\nL739: \nL740: result.ref = github.context.ref\nL741: \nL742: result.commit = github.context.sha\nL743: \nL744: // Some events have an unqualifed ref. For example when a PR is merged (pull_request closed event),\nL745: \nL746: // the ref is unqualifed like \"main\" instead of \"refs/heads/main\".\nL747: \nL748: if (result.commit && result.ref && !result.ref.startsWith('refs/')) {\nL749: \nL750: result.ref = `refs/heads/${result.ref}`\nL751: \nL752: }\nL753: \nL754: }\nL755: \nL756: }\nL757: \nL758: // SHA?\nL759: else if (asciiTrimmedRef.match(/^[0-9a-fA-F]{40}$/)) {\nL760: \nL761: result.commit = asciiTrimmedRef\nL762: \nL763: result.ref = ''\nL764: \nL765: }\nL766: \nL767: core.debug(`ref = '${result.ref}'`)\nL768: \nL769: core.debug(`commit = '${result.commit}'`)\nL770: \nL771: // Clean\nL772: \nL773: result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'\nL774: \nL775: core.debug(`clean = ${result.clean}`)\nL776: \nL777: // Filter\nL778: \nL779: const filter = core.getInput('filter')\nL780: \nL781: if (filter) {\nL782: \nL783: result.filter = filter\nL784: \nL785: }\nL786: \nL787: core.debug(`filter = ${result.filter}`)\nL788: \nL789: // Sparse checkout\nL790: const sparseCheckout = core.getMultilineInput('sparse-checkout')\nL791: \nL792: if (sparseCheckout.length) {\nL793: \nL794: result.sparseCheckout = sparseCheckout\nL795: \nL796: core.debug(`sparse checkout = ${result.sparseCheckout}`)\nL797: \nL798: }\nL799: \nL800: result.sparseCheckoutConeMode =\nL801: \nL802: (core.getInput('sparse-checkout-cone-mode') || 'true').toUpperCase() ===\nL803: \nL804: 'TRUE'\nL805: \nL806: // Fetch depth\nL807: \nL808: result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))\nL809: \nL810: if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {\nL811: \nL812: result.fetchDepth = 0\nL813: \nL814: }\nL815: core.debug(`fetch depth = ${result.fetchDepth}`)\nL816: \nL817: // Fetch tags\nL818: \nL819: result.fetchTags =\nL820: \nL821: (core.getInput('fetch-tags') || 'false').toUpperCase() === 'TRUE'\nL822: \nL823: core.debug(`fetch tags = ${result.fetchTags}`)\nL824: \nL825: // Show fetch progress\nL826: \nL827: result.showProgress =\nL828: \nL829: (core.getInput('show-progress') || 'true').toUpperCase() === 'TRUE'\nL830: \nL831: core.debug(`show progress = ${result.showProgress}`)\nL832: \nL833: // LFS\nL834: \nL835: result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'\nL836: \nL837: core.debug(`lfs = ${result.lfs}`)\nL838: \nL839: // Submodules\n"}}
{"i":1,"result":{"status":"fulfilled","value":{"chunk_id":"1693e0","wall_time_seconds":0.000002542,"exit_code":0,"original_token_count":755,"output":"name: Codex Review\n\n# Automated independent second-model review, formalized from\n# docs/production-hardening-backlog.md item #4 (\"Independent second-model\n# review as a standing practice\"). Runs Codex (a model with no stake in this\n# repo's prior conclusions) against every PR diff and posts its findings as a\n# PR comment. This job is advisory: it reports [P1]/[P2] findings and fails\n# the check on a [P1], but it does not itself block merge unless branch\n# protection is separately configured to require it — that's a deliberate,\n# explicit decision left to the repo owner, not made here.\n#\n# Requires ONE of:\n# - CODEX_AUTH_JSON: the contents of a `codex login`-produced ~/.codex/auth.json\n# (ChatGPT-subscription auth — ties usage to that account's plan, not a\n# separate API budget; see docs/production-hardening-backlog.md item #4)\n# - CODEX_API_KEY / OPENAI_API_KEY: a standard API key\n# Without one of these, this job reports that plainly and exits — it never\n# silently no-ops as if review happened. Secrets are only available to\n# `pull_request` runs triggered from THIS repo (never from a fork, by GitHub\n# Actions' own default security model — this workflow deliberately uses\n# `pull_request`, not `pull_request_target`, to keep that protection).\n#\n# KNOWN RESIDUAL RISK, not fully closed by this workflow: the reviewing\n# agent runs with read access to its own auth material while reviewing a\n# same-repo PR's diff, which could contain a prompt-injection attempt asking\n# it to reveal that material in its review output. This workflow redacts any\n# verbatim occurrence of the configured secret value from the posted comment\n# and job logs as a concrete backstop, but that is a partial mitigation, not\n# full isolation (a sufficiently reworded/re-encoded exfiltration attempt\n# could still evade a literal-substring redaction). Real isolation would mean\n# authenticating through a proxy that never hands the credential itself to\n# the process reading untrusted diff content — not implemented here. Treat\n# CODEX_AUTH_JSON as a real secret with this residual exposure in mind, not\n# as fully contained by this job.\n\non:\n pull_request:\n types: [opened, synchronize, reopened]\n branches: [main]\n\npermissions:\n contents: read\n pull-requests: write\n\nconcurrency:\n group: codex-review-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n\njobs:\n codex-review:\n name: Codex independent review\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n # persist-credentials: false — this job's own GITHUB_TOKEN\n # (pull-requests: write) must not be left readable by the same agent\n # that reads untrusted PR diff content; this job never needs to push.\n - name: Checkout\n uses: actions/checkout@v6\n with:\n fetch-depth: 0\n persist-credentials: false\n\n # Secrets are passed via env:, never interpolated with ${{ }} directly\n # into the script body — GitHub Actions writes a run: step's script to\n"}}}
{"i":2,"result":{"status":"fulfilled","value":{"chunk_id":"5edf9a","wall_time_seconds":0.000001625,"exit_code":0,"original_token_count":1087,"output":"scripts/analyze-latest-review.py:2:\"\"\"Decide, from evidence, whether a just-completed review round newly\nscripts/analyze-latest-review.py:5:This is the piece that closes the gap named while building this archive:\nscripts/analyze-latest-review.py:7:mechanism recommendation from the archive's accumulated data, but something\nscripts/analyze-latest-review.py:12:recommendation with vs. without the latest round's findings included, and\nscripts/analyze-latest-review.py:14:\"mechanism\" (or newly appears at/above threshold) because of this round\nscripts/analyze-latest-review.py:16:rounds, which would otherwise fire on every single subsequent round\nscripts/analyze-latest-review.py:19:have read the archive and noticed the pattern themselves.\nscripts/analyze-latest-review.py:22: python3 analyze-latest-review.py <archive.jsonl> <review-comment.txt> [--threshold N]\nscripts/analyze-latest-review.py:51:def load_archive(path: str) -> list[dict]:\nscripts/analyze-latest-review.py:61:def next_round_number(entries: list[dict]) -> int:\nscripts/analyze-latest-review.py:62: rounds = [e.get(\"round\", 0) for e in entries]\nscripts/analyze-latest-review.py:63: return (max(rounds) + 1) if rounds else 1\nscripts/analyze-latest-review.py:72: archive_entries: list[dict], new_findings: list[str], threshold: int\nscripts/analyze-latest-review.py:74: \"\"\"Compare recommendations with vs. without the new round's findings.\nscripts/analyze-latest-review.py:77: round is included -- i.e. this round is the one that tipped it over,\nscripts/analyze-latest-review.py:78: not a topic that already exceeded the threshold in prior rounds.\nscripts/analyze-latest-review.py:80: before = recommendations_by_topic(archive_entries, threshold)\nscripts/analyze-latest-review.py:82: new_round_entry = {\"round\": next_round_number(archive_entries), \"findings\": new_findings}\nscripts/analyze-latest-review.py:83: after_entries = [*archive_entries, new_round_entry]\nscripts/analyze-latest-review.py:99: parser.add_argument(\"archive_path\")\nscripts/analyze-latest-review.py:104: archive_entries = load_archive(args.archive_path)\nscripts/analyze-latest-review.py:116: newly_crossed = find_newly_crossed_topics(archive_entries, new_findings, args.threshold)\nscripts/analyze-latest-review.py:122: f\"as of this round's findings.\"\nscripts/analyze-latest-review.py:125: print(\"No topic newly crosses the mechanism-fix threshold this round.\")\n.github/workflows/codex-review.yml:51: name: Codex independent review\n.github/workflows/codex-review.yml:299: await github.rest.issues.createComment({\n.github/workflows/codex-review.yml:303: body: `### Codex independent review\\n\\n${body}\\n\\n---\\n*Generated by an independent model reviewing this diff with no stake in prior conclusions on it — see [docs/production-hardening-backlog.md](../blob/main/docs/production-hardening-backlog.md) item #4.*`,\n.github/workflows/archive-and-recommend.yml:6:# docs/self-improvement-archive.jsonl's accumulated evidence, and\n.github/workflows/archive-and-recommend.yml:7:# scripts/analyze-latest-review.py could tell whether the LATEST round is\n.github/workflows/archive-and-recommend.yml:11:# archive. This workflow makes it automatic, but ONLY for a deliberately\n.github/workflows/archive-and-recommend.yml:21:# it reads (the posted review comment, the archive file) is already public,\n.github/workflows/archive-and-recommend.yml:34: group: archive-and-recommend-${{ github.event.workflow_run.id }}\n.github/workflows/archive-and-recommend.yml:66: (c.body || \"\").startsWith(\"### Codex independent review\")\n.github/workflows/archive-and-recommend.yml:83: docs/self-improvement-archive.jsonl \\\n.github/workflows/archive-and-recommend.yml:124: `Automatically opened by [\\`archive-and-recommend.yml\\`](../blob/main/.github/workflows/archive-and-recommend.yml).`,\n.github/workflows/archive-and-recommend.yml:127: `threshold in \\`docs/self-improvement-archive.jsonl\\`, as of the`,\n.github/workflows/archive-and-recommend.yml:133: `Run \\`python3 scripts/detect-recurring-pattern.py docs/self-improvement-archive.jsonl\\``,\n.github/workflows/archive-and-recommend.yml:134: `for the full evidence trail (which rounds, which findings) behind this`,\n"}}}
{"i":3,"result":{"status":"fulfilled","value":{"chunk_id":"f360e7","wall_time_seconds":0.000001458,"exit_code":1,"original_token_count":0,"output":""}}}>curl -s --max-time 15 'https://r.jina.ai/https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows' | rg -A 18 '^workflow_run'{"i":0,"result":{"status":"fulfilled","value":"Events that trigger workflows - GitHub Docs (https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows)\nciteturn1view0 [wordlim: 200] Crawled: today; Content type: text/html; Source: find({\"ref_id\":\"turn0view0\",\"pattern\":\"## workflow_run\"}); Total lines: 1321\nL1177: If you run this workflow from a browser you must enter values for the required inputs manually before the workflow will run.\nL1178: \nL1179: cite161†Image: Screenshot of a list of workflow runs. A dropdown menu, labeled \"Run workflow\" and expanded to show input fields, is outlined in dark orange. L1180: \nL1181: You can also pass inputs when you run a workflow from a script, or by using GitHub CLI. For example:\nL1182: \nL1183: `gh workflow run run-tests.yml -f logLevel=warning -f tags=false -f environment=staging\nL1184: `\nL1185: For more information, see the GitHub CLI information in cite158†Manually running a workflow .\nL1186: ## cite39†`workflow_run` L1187: \nL1188: Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF`\nL1189: --- | --- | --- | ---\nL1190: cite162†`workflow_run` | - `completed`\nL1191: - `requested`\nL1192: - `in_progress` | Last commit on default branch | Default branch\nL1193: \nL1194: Note\nL1195: * More than one activity type triggers this event. The `requested` activity type does not occur when a workflow is re-run. For information about each activity type, see cite162†Webhook events and payloads . By default, all activity types trigger workflows that run on this event. You can limit your workflow runs to specific activity types using the `types` keyword. For more information, see cite44†Workflow syntax for GitHub Actions .\nL1196: * This event will only trigger a workflow run if the workflow file exists on the default branch.\nL1197: * You can't use `workflow_run` to chain together more than three levels of workflows. For example, if you attempt to trigger five workflows (named `B` to `F`) to run sequentially after an initial workflow `A` has run (that is: `A` → `B` → `C` → `D` → `E` → `F`), workflows `E` and `F` will not be run.\nL1198: This event occurs when a workflow run is requested or completed. It allows you to execute a workflow based on execution or completion of another workflow. The workflow started by the `workflow_run` event is able to access secrets and write tokens, even if the previous workflow was not. This is useful in cases where the previous workflow is intentionally not privileged, but you need to take a privileged action in a later workflow.\nL1199: \nL1200: Warning\nL1201: Running untrusted code on the `workflow_run` trigger may lead to security vulnerabilities. These vulnerabilities include cache poisoning and granting unintended access to write privileges or secrets. For more information, see cite123†Secure use reference in the GitHub Enterprise Cloud documentation, and cite124†Preventing pwn requests†securitylab.github.com on the GitHub Security Lab website.\nL1202: In this example, a workflow is configured to run after the separate \"Run Tests\" workflow completes.\nL1203: \nL1204: `on:\nL1205: workflow_run:\nL1206: workflows: [Run Tests]\nL1207: types:\nL1208: - completed\nL1209: `\nL1210: If you specify multiple `workflows` for the `workflow_run` event, only one of the workflows needs to run. For example, a workflow with the following trigger will run whenever the \"Staging\" workflow or the \"Lab\" workflow completes.\nL1211: \nL1212: `on:\nL1213: workflow_run:\nL1214: workflows: [Staging, Lab]\nL1215: types:\nL1216: - completed\nL1217: `\nL1218: ### cite163†Running a workflow based on the conclusion of another workflow L1219: A workflow run is triggered regardless of the conclusion of the previous workflow. If you want to run a job or step based on the result of the triggering workflow, you can use a conditional with the `github.event.workflow_run.conclusion` property. For example, this workflow will run whenever a workflow named \"Build\" completes, but the `on-success` job will only run if the \"Build\" workflow succeeded, and the `on-failure` job will only run if the \"Build\" workflow failed:\nL1220: \nL1221: `on:\nL1222: workflow_run:\nL1223: workflows: [Build]\nL1224: types: [completed]\nL1225: \nL1226: jobs:\nL1227: on-success:\nL1228: runs-on: ubuntu-latest\nL1229: if: ${{ github.event.workflow_run.conclusion == 'success' }}\nL1230: steps:\nL1231: - run: echo 'The triggering workflow passed'\nL1232: on-failure:\nL1233: runs-on: ubuntu-latest\nL1234: if: ${{ github.event.workflow_run.conclusion == 'failure' }}\nL1235: steps:\nL1236: - run: echo 'The triggering workflow failed'\nL1237: `\nL1238: ### cite164†Limiting your workflow to run based on branches L1239: You can use the `branches` or `branches-ignore` filter to specify what branches the triggering workflow must run on in order to trigger your workflow. For more information, see cite165†Workflow syntax for GitHub Actions . For example, a workflow with the following trigger will only run when the workflow named `Build` runs on a branch named `canary`.\nL1240: \nL1241: `on:\nL1242: workflow_run:\nL1243: workflows: [Build]\nL1244: types: [requested]\nL1245: branches: [canary]\nL1246: `\nL1247: ### cite166†Using data from the triggering workflow L1248: \nL1249: You can access the cite162†`workflow_run` event payload that corresponds to the workflow that triggered your workflow. For example, if your triggering workflow generates artifacts, a workflow triggered with the `workflow_run` event can access these artifacts.\nL1250: The following workflow uploads data as an artifact. (In this simplified example, the data is the pull request number.)\nL1251: \nL1252: `name: Upload data\nL1253: \nL1254: on:\nL1255: pull_request:\nL1256: \nL1257: jobs:\nL1258: upload:\nL1259: runs-on: ubuntu-latest\nL1260: \nL1261: steps:\nL1262: - name: Save PR number\nL1263: env:\nL1264: PR_NUMBER: ${{ github.event.number }}\nL1265: run: |\nL1266: mkdir -p ./pr\nL1267: echo $PR_NUMBER > ./pr/pr_number\nL1268: - uses: actions/upload-artifact@v4\nL1269: with:\nL1270: name: pr_number\nL1271: path: pr/\nL1272: `\nL1273: When a run of the above workflow completes, it triggers a run of the following workflow. The following workflow uses the `github.event.workflow_run` context and the actions/download-artifact@v5 action to download the artifact that was uploaded by the above workflow, then comments on the pull request whose number was uploaded as an artifact.\nL1274: \nL1275: `name: Use the data\nL1276: \nL1277: on:\nL1278: workflow_run:\nL1279: workflows: [Upload data]\nL1280: types:\nL1281: - completed\nL1282: \nL1283: jobs:\nL1284: download:\nL1285: runs-on: ubuntu-latest\nL1286: permissions:\nL1287: actions: read\nL1288: issues: write\nL1289: steps:\nL1290: - name: 'Download artifact'\nL1291: uses: actions/download-artifact@v5\nL1292: with:\nL1293: name: pr_number\nL1294: # do not extract in the workspace dir that may contain executable scripts\nL1295: path: ${{ runner.temp }}/artifacts\nL1296: run-id: ${{ github.event.workflow_run.id }}\nL1297: github-token: ${{ secrets.GITHUB_TOKEN }}\nL1298: - name: 'Comment on PR'\nL1299: uses: actions/github-script@v8\nL1300: with:\nL1301: github-token: ${{ secrets.GITHUB_TOKEN }}\nL1302: script: |\nL1303: const fs = require('fs');\nL1304: const path = require('path');\nL1305: const temp = '${{ runner.temp }}/artifacts';\nL1306: const issue_number_raw = fs.readFileSync(path.join(temp, 'pr_number'), 'utf8').trim();\nL1307: const issue_number = Number(issue_number_raw);\nL1308: if (!Number.isInteger(issue_number)) {\nL1309: throw new Error(`Invalid PR number in pr_number artifact: \"${issue_number_raw}\"`);\nL1310: }\nL1311: await github.rest.issues.createComment({\nL1312: owner: context.repo.owner,\nL1313: repo: context.repo.repo,\nL1314: issue_number: issue_number,\nL1315: body: 'Thank you for the PR!'\nL1316: });\n--------------------------------------------------------------------------------\ncheckout/src/input-helper.ts at v4 · actions/checkout · GitHub (https://github.com/actions/checkout/blob/v4/src/input-helper.ts)\nciteturn1view1 [wordlim: 200] Crawled: today; Content type: text/html; Source: find({\"ref_id\":\"turn0view1\",\"pattern\":\"result.ref = github.context.ref\"}); Total lines: 939\nL636: `${github.context.repo.owner}/${github.context.repo.repo}`\nL637: \nL638: core.debug(`qualified repository = '${qualifiedRepository}'`)\nL639: \nL640: const splitRepository = qualifiedRepository.split('/')\nL641: \nL642: if (\nL643: \nL644: splitRepository.length !== 2 ||\nL645: \nL646: !splitRepository[0] ||\nL647: \nL648: !splitRepository[1]\nL649: \nL650: ) {\nL651: \nL652: throw new Error(\nL653: \nL654: `Invalid repository '${qualifiedRepository}'. Expected format {owner}/{repo}.`\nL655: \nL656: )\nL657: \nL658: }\nL659: \nL660: result.repositoryOwner = splitRepository[0]\nL661: \nL662: result.repositoryName = splitRepository[1]\nL663: \nL664: // Repository path\nL665: result.repositoryPath = core.getInput('path') || '.'\nL666: \nL667: result.repositoryPath = path.resolve(\nL668: \nL669: githubWorkspacePath,\nL670: \nL671: result.repositoryPath\nL672: \nL673: )\nL674: \nL675: if (\nL676: \nL677: !(result.repositoryPath + path.sep).startsWith(\nL678: \nL679: githubWorkspacePath + path.sep\nL680: \nL681: )\nL682: \nL683: ) {\nL684: \nL685: throw new Error(\nL686: \nL687: `Repository path '${result.repositoryPath}' is not under '${githubWorkspacePath}'`\nL688: \nL689: )\nL690: \nL691: }\nL692: \nL693: // Workflow repository?\nL694: \nL695: const isWorkflowRepository =\nL696: \nL697: qualifiedRepository.toUpperCase() ===\nL698: \nL699: `${github.context.repo.owner}/${github.context.repo.repo}`.toUpperCase()\nL700: // Source branch, source version\nL701: \nL702: result.ref = core.getInput('ref')\nL703: \nL704: // core.getInput()'s default trim strips a range of Unicode characters such as a\nL705: \nL706: // leading BOM (U+FEFF) or NBSP (U+00A0). Those are valid in a git ref name, so\nL707: \nL708: // a fork branch named \"<BOM>\" + 40 hex chars would trim down to a bare SHA and\nL709: \nL710: // be silently reclassified as a commit, bypassing the unsafe fork PR checkout\nL711: \nL712: // guard.\nL713: \nL714: //\nL715: \nL716: // The trim below strips only the ASCII whitespace characters which are all forbidden\nL717: // in a git branch name.\nL718: \nL719: // \\t U+0009 horizontal tab - ASCII control, forbidden in ref names\nL720: \nL721: // \\n U+000A line feed - ASCII control, forbidden in ref names\nL722: \nL723: // \\v U+000B vertical tab - ASCII control, forbidden in ref names\nL724: \nL725: // \\f U+000C form feed - ASCII control, forbidden in ref names\nL726: \nL727: // \\r U+000D carriage return - ASCII control, forbidden in ref names\nL728: \nL729: // ' ' U+0020 space - forbidden in ref names\nL730: \nL731: const asciiTrimmedRef = core\nL732: \nL733: .getInput('ref', {trimWhitespace: false})\nL734: .replace(/^[\\t\\n\\v\\f\\r ]+|[\\t\\n\\v\\f\\r ]+$/g, '')\nL735: \nL736: if (!result.ref) {\nL737: \nL738: if (isWorkflowRepository) {\nL739: \nL740: result.ref = github.context.ref\nL741: \nL742: result.commit = github.context.sha\nL743: \nL744: // Some events have an unqualifed ref. For example when a PR is merged (pull_request closed event),\nL745: \nL746: // the ref is unqualifed like \"main\" instead of \"refs/heads/main\".\nL747: \nL748: if (result.commit && result.ref && !result.ref.startsWith('refs/')) {\nL749: \nL750: result.ref = `refs/heads/${result.ref}`\nL751: \nL752: }\nL753: \nL754: }\nL755: \nL756: }\nL757: \nL758: // SHA?\nL759: else if (asciiTrimmedRef.match(/^[0-9a-fA-F]{40}$/)) {\nL760: \nL761: result.commit = asciiTrimmedRef\nL762: \nL763: result.ref = ''\nL764: \nL765: }\nL766: \nL767: core.debug(`ref = '${result.ref}'`)\nL768: \nL769: core.debug(`commit = '${result.commit}'`)\nL770: \nL771: // Clean\nL772: \nL773: result.clean = (core.getInput('clean') || 'true').toUpperCase() === 'TRUE'\nL774: \nL775: core.debug(`clean = ${result.clean}`)\nL776: \nL777: // Filter\nL778: \nL779: const filter = core.getInput('filter')\nL780: \nL781: if (filter) {\nL782: \nL783: result.filter = filter\nL784: \nL785: }\nL786: \nL787: core.debug(`filter = ${result.filter}`)\nL788: \nL789: // Sparse checkout\nL790: const sparseCheckout = core.getMultilineInput('sparse-checkout')\nL791: \nL792: if (sparseCheckout.length) {\nL793: \nL794: result.sparseCheckout = sparseCheckout\nL795: \nL796: core.debug(`sparse checkout = ${result.sparseCheckout}`)\nL797: \nL798: }\nL799: \nL800: result.sparseCheckoutConeMode =\nL801: \nL802: (core.getInput('sparse-checkout-cone-mode') || 'true').toUpperCase() ===\nL803: \nL804: 'TRUE'\nL805: \nL806: // Fetch depth\nL807: \nL808: result.fetchDepth = Math.floor(Number(core.getInput('fetch-depth') || '1'))\nL809: \nL810: if (isNaN(result.fetchDepth) || result.fetchDepth < 0) {\nL811: \nL812: result.fetchDepth = 0\nL813: \nL814: }\nL815: core.debug(`fetch depth = ${result.fetchDepth}`)\nL816: \nL817: // Fetch tags\nL818: \nL819: result.fetchTags =\nL820: \nL821: (core.getInput('fetch-tags') || 'false').toUpperCase() === 'TRUE'\nL822: \nL823: core.debug(`fetch tags = ${result.fetchTags}`)\nL824: \nL825: // Show fetch progress\nL826: \nL827: result.showProgress =\nL828: \nL829: (core.getInput('show-progress') || 'true').toUpperCase() === 'TRUE'\nL830: \nL831: core.debug(`show progress = ${result.showProgress}`)\nL832: \nL833: // LFS\nL834: \nL835: result.lfs = (core.getInput('lfs') || 'false').toUpperCase() === 'TRUE'\nL836: \nL837: core.debug(`lfs = ${result.lfs}`)\nL838: \nL839: // Submodules\n"}}
{"i":1,"result":{"status":"fulfilled","value":{"chunk_id":"1693e0","wall_time_seconds":0.000002542,"exit_code":0,"original_token_count":755,"output":"name: Codex Review\n\n# Automated independent second-model review, formalized from\n# docs/production-hardening-backlog.md item #4 (\"Independent second-model\n# review as a standing practice\"). Runs Codex (a model with no stake in this\n# repo's prior conclusions) against every PR diff and posts its findings as a\n# PR comment. This job is advisory: it reports [P1]/[P2] findings and fails\n# the check on a [P1], but it does not itself block merge unless branch\n# protection is separately configured to require it — that's a deliberate,\n# explicit decision left to the repo owner, not made here.\n#\n# Requires ONE of:\n# - CODEX_AUTH_JSON: the contents of a `codex login`-produced ~/.codex/auth.json\n# (ChatGPT-subscription auth — ties usage to that account's plan, not a\n# separate API budget; see docs/production-hardening-backlog.md item #4)\n# - CODEX_API_KEY / OPENAI_API_KEY: a standard API key\n# Without one of these, this job reports that plainly and exits — it never\n# silently no-ops as if review happened. Secrets are only available to\n# `pull_request` runs triggered from THIS repo (never from a fork, by GitHub\n# Actions' own default security model — this workflow deliberately uses\n# `pull_request`, not `pull_request_target`, to keep that protection).\n#\n# KNOWN RESIDUAL RISK, not fully closed by this workflow: the reviewing\n# agent runs with read access to its own auth material while reviewing a\n# same-repo PR's diff, which could contain a prompt-injection attempt asking\n# it to reveal that material in its review output. This workflow redacts any\n# verbatim occurrence of the configured secret value from the posted comment\n# and job logs as a concrete backstop, but that is a partial mitigation, not\n# full isolation (a sufficiently reworded/re-encoded exfiltration attempt\n# could still evade a literal-substring redaction). Real isolation would mean\n# authenticating through a proxy that never hands the credential itself to\n# the process reading untrusted diff content — not implemented here. Treat\n# CODEX_AUTH_JSON as a real secret with this residual exposure in mind, not\n# as fully contained by this job.\n\non:\n pull_request:\n types: [opened, synchronize, reopened]\n branches: [main]\n\npermissions:\n contents: read\n pull-requests: write\n\nconcurrency:\n group: codex-review-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n\njobs:\n codex-review:\n name: Codex independent review\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n # persist-credentials: false — this job's own GITHUB_TOKEN\n # (pull-requests: write) must not be left readable by the same agent\n # that reads untrusted PR diff content; this job never needs to push.\n - name: Checkout\n uses: actions/checkout@v6\n with:\n fetch-depth: 0\n persist-credentials: false\n\n # Secrets are passed via env:, never interpolated with ${{ }} directly\n # into the script body — GitHub Actions writes a run: step's script to\n"}}}
{"i":2,"result":{"status":"fulfilled","value":{"chunk_id":"5edf9a","wall_time_seconds":0.000001625,"exit_code":0,"original_token_count":1087,"output":"scripts/analyze-latest-review.py:2:\"\"\"Decide, from evidence, whether a just-completed review round newly\nscripts/analyze-latest-review.py:5:This is the piece that closes the gap named while building this archive:\nscripts/analyze-latest-review.py:7:mechanism recommendation from the archive's accumulated data, but something\nscripts/analyze-latest-review.py:12:recommendation with vs. without the latest round's findings included, and\nscripts/analyze-latest-review.py:14:\"mechanism\" (or newly appears at/above threshold) because of this round\nscripts/analyze-latest-review.py:16:rounds, which would otherwise fire on every single subsequent round\nscripts/analyze-latest-review.py:19:have read the archive and noticed the pattern themselves.\nscripts/analyze-latest-review.py:22: python3 analyze-latest-review.py <archive.jsonl> <review-comment.txt> [--threshold N]\nscripts/analyze-latest-review.py:51:def load_archive(path: str) -> list[dict]:\nscripts/analyze-latest-review.py:61:def next_round_number(entries: list[dict]) -> int:\nscripts/analyze-latest-review.py:62: rounds = [e.get(\"round\", 0) for e in entries]\nscripts/analyze-latest-review.py:63: return (max(rounds) + 1) if rounds else 1\nscripts/analyze-latest-review.py:72: archive_entries: list[dict], new_findings: list[str], threshold: int\nscripts/analyze-latest-review.py:74: \"\"\"Compare recommendations with vs. without the new round's findings.\nscripts/analyze-latest-review.py:77: round is included -- i.e. this round is the one that tipped it over,\nscripts/analyze-latest-review.py:78: not a topic that already exceeded the threshold in prior rounds.\nscripts/analyze-latest-review.py:80: before = recommendations_by_topic(archive_entries, threshold)\nscripts/analyze-latest-review.py:82: new_round_entry = {\"round\": next_round_number(archive_entries), \"findings\": new_findings}\nscripts/analyze-latest-review.py:83: after_entries = [*archive_entries, new_round_entry]\nscripts/analyze-latest-review.py:99: parser.add_argument(\"archive_path\")\nscripts/analyze-latest-review.py:104: archive_entries = load_archive(args.archive_path)\nscripts/analyze-latest-review.py:116: newly_crossed = find_newly_crossed_topics(archive_entries, new_findings, args.threshold)\nscripts/analyze-latest-review.py:122: f\"as of this round's findings.\"\nscripts/analyze-latest-review.py:125: print(\"No topic newly crosses the mechanism-fix threshold this round.\")\n.github/workflows/codex-review.yml:51: name: Codex independent review\n.github/workflows/codex-review.yml:299: await github.rest.issues.createComment({\n.github/workflows/codex-review.yml:303: body: `### Codex independent review\\n\\n${body}\\n\\n---\\n*Generated by an independent model reviewing this diff with no stake in prior conclusions on it — see [docs/production-hardening-backlog.md](../blob/main/docs/production-hardening-backlog.md) item #4.*`,\n.github/workflows/archive-and-recommend.yml:6:# docs/self-improvement-archive.jsonl's accumulated evidence, and\n.github/workflows/archive-and-recommend.yml:7:# scripts/analyze-latest-review.py could tell whether the LATEST round is\n.github/workflows/archive-and-recommend.yml:11:# archive. This workflow makes it automatic, but ONLY for a deliberately\n.github/workflows/archive-and-recommend.yml:21:# it reads (the posted review comment, the archive file) is already public,\n.github/workflows/archive-and-recommend.yml:34: group: archive-and-recommend-${{ github.event.workflow_run.id }}\n.github/workflows/archive-and-recommend.yml:66: (c.body || \"\").startsWith(\"### Codex independent review\")\n.github/workflows/archive-and-recommend.yml:83: docs/self-improvement-archive.jsonl \\\n.github/workflows/archive-and-recommend.yml:124: `Automatically opened by [\\`archive-and-recommend.yml\\`](../blob/main/.github/workflows/archive-and-recommend.yml).`,\n.github/workflows/archive-and-recommend.yml:127: `threshold in \\`docs/self-improvement-archive.jsonl\\`, as of the`,\n.github/workflows/archive-and-recommend.yml:133: `Run \\`python3 scripts/detect-recurring-pattern.py docs/self-improvement-archive.jsonl\\``,\n.github/workflows/archive-and-recommend.yml:134: `for the full evidence trail (which rounds, which findings) behind this`,\n"}}}
{"i":3,"result":{"status":"fulfilled","value":{"chunk_id":"f360e7","wall_time_seconds":0.000001458,"exit_code":1,"original_token_count":0,"output":""}}}The checkout claim holds for this file: workflow_run supplies the default-branch ref and SHA, and checkout has no override. I found separate problems with accepting unauthenticated comments, selecting reviews unrelated to the completed run, and allowing concurrent issue creation. I’m checking the threshold logic with small examples.
>python3 -B - <<'PY'
import importlib.util
from pathlib import Path
p = Path('scripts/analyze-latest-review.py')
spec = importlib.util.spec_from_file_location('review_analysis', p)
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
e = lambda n: {'round': n, 'findings': ['**[P2]** shell exit code ignored']}
f = ['**[P2]** shell exit code ignored']
print('Two rounds already archived, replay round 2:', m.find_newly_crossed_topics([e(1), e(2)], f, 3))
print('One archived round, three independent invocations:', [m.find_newly_crossed_topics([e(1)], f, 3) for _ in range(3)])
print('Two archived rounds, three independent invocations:', [m.find_newly_crossed_topics([e(1), e(2)], f, 3) for _ in range(3)])
print('Actual archive topic counts:', [(r['topic'], r['recurrence_count']) for r in m.detect.analyze(m.load_archive('docs/self-improvement-archive.jsonl'), 3)['recommendations']])
print('Classifications:', [(s, m.detect.classify_finding(s)) for s in ['refresh token expires', 'fork PR GITHUB_TOKEN is read-only', 'persist-credentials: false', 'environment variable precedence']])
PY{"chunk_id":"d72317","wall_time_seconds":0,"exit_code":1,"original_token_count":19,"output":"zsh:1: can't create temp file for here document: operation not permitted\n"}>python3 -B -c 'import importlib.util
from pathlib import Path
p = Path("scripts/analyze-latest-review.py")
spec = importlib.util.spec_from_file_location("review_analysis", p)
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
e = lambda n: {"round": n, "findings": ["**[P2]** shell exit code ignored"]}
f = ["**[P2]** shell exit code ignored"]
print("Replay already archived round 2:", m.find_newly_crossed_topics([e(1), e(2)], f, 3))
print("One archived round, three invocations:", [m.find_newly_crossed_topics([e(1)], f, 3) for _ in range(3)])
print("Two archived rounds, three invocations:", [m.find_newly_crossed_topics([e(1), e(2)], f, 3) for _ in range(3)])
print("Actual archive counts:", [(r["topic"], r["recurrence_count"]) for r in m.detect.analyze(m.load_archive("docs/self-improvement-archive.jsonl"), 3)["recommendations"]])
'{"chunk_id":"9a61a7","wall_time_seconds":0.000003125,"exit_code":0,"original_token_count":125,"output":"Replay already archived round 2: [{'topic': 'shell-semantics', 'recommended_action': 'mechanism'}]\nOne archived round, three invocations: [[], [], []]\nTwo archived rounds, three invocations: [[{'topic': 'shell-semantics', 'recommended_action': 'mechanism'}], [{'topic': 'shell-semantics', 'recommended_action': 'mechanism'}], [{'topic': 'shell-semantics', 'recommended_action': 'mechanism'}]]\nActual archive counts: [('credential-redaction', 6), ('shell-semantics', 2), ('env-var-precedence', 1)]\n"}Checkout is safe as written: workflow_run supplies the default-branch ref/SHA, which checkout uses without a ref override. Both API queries paginate; no comment-to-code execution or merge/deploy path was found.
find_newly_crossed_topics() always assigns max(round)+1 without checking review identity; replaying a review already represented in a two-round archive fabricates a third distinct round and falsely crosses the threshold.No content
- name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.12"
- name: Fetch latest Codex review comment for the PR id: fetch-comment uses: actions/github-script@v7 with: script: | const prNumber = context.payload.workflow_run.pull_requests[0].number; const comments = await github.paginate(github.rest.issues.listComments, { owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, per_page: 100, }); const reviewComments = comments.filter((c) => (c.body || "").startsWith("### Codex independent review") ); if (reviewComments.length === 0) { core.setOutput("found", "false"); return; } const latest = reviewComments[reviewComments.length - 1]; const fs = require("fs"); fs.writeFileSync(process.env.RUNNER_TEMP + "/review-comment.txt", latest.body, "utf8"); core.setOutput("found", "true"); core.setOutput("pr-number", String(prNumber));
- name: Analyze for newly-crossed recurrence thresholds id: analyze if: steps.fetch-comment.outputs.found == 'true' run: | python3 scripts/analyze-latest-review.py \ docs/self-improvement-archive.jsonl \ "$RUNNER_TEMP/review-comment.txt" \ > "$RUNNER_TEMP/analysis.txt" cat "$RUNNER_TEMP/analysis.txt" # Extract just the JSON payload after the `---` separator. awk '/^---$/{found=1; next} found' "$RUNNER_TEMP/analysis.txt" > "$RUNNER_TEMP/analysis.json" cat "$RUNNER_TEMP/analysis.json"
- name: Open a tracking issue for each newly-crossed topic if: steps.fetch-comment.outputs.found == 'true' uses: actions/github-script@v7 with: script: | const fs = require("fs"); const prNumber = "${{ steps.fetch-comment.outputs.pr-number }}"; const raw = fs.readFileSync(process.env.RUNNER_TEMP + "/analysis.json", "utf8"); const { newly_crossed: newlyCrossed } = JSON.parse(raw);
if (!newlyCrossed || newlyCrossed.length === 0) { console.log("No topic newly crossed the mechanism-fix threshold. Nothing to do."); return; }
for (const item of newlyCrossed) { const title = `Recurring pattern: ${item.topic} — mechanism-level fix recommended`;
// Idempotency: don't open a second issue for a topic that // already has an open tracking issue. const existing = await github.paginate(github.rest.issues.listForRepo, { owner: context.repo.owner, repo: context.repo.repo, state: "open", labels: "self-improvement-recommendation", per_page: 100, }); if (existing.some((issue) => issue.title === title)) { console.log(`Issue already open for topic "${item.topic}" — skipping.`); continue; }
const body = [ `Automatically opened by [\`archive-and-recommend.yml\`](../blob/main/.github/workflows/archive-and-recommend.yml).`, ``, `A finding topic newly crossed the mechanism-level-fix recurrence`, `threshold in \`docs/self-improvement-archive.jsonl\`, as of the`, `Codex review on PR #${prNumber}.`, ``, `**Topic:** \`${item.topic}\``, `**Recommended action:** ${item.recommended_action}-level fix`, ``, `Run \`python3 scripts/detect-recurring-pattern.py docs/self-improvement-archive.jsonl\``, `for the full evidence trail (which rounds, which findings) behind this`, `recommendation.`, ``, `This issue was opened automatically. Deciding what the mechanism-level`, `fix should be, and merging/deploying it, remains a human decision —`, `this workflow's only permission is \`issues: write\`.`, ].join("\n");
await github.rest.issues.create({ owner: context.repo.owner, repo: context.repo.repo, title, body, labels: ["self-improvement-recommendation"], }); console.log(`Opened tracking issue for topic "${item.topic}".`); }Returns entries for topics that recommend "mechanism" only once the newround is included -- i.e. this round is the one that tipped it over,not a topic that already exceeded the threshold in prior rounds."""before = recommendations_by_topic(archive_entries, threshold)
new_round_entry = {"round": next_round_number(archive_entries), "findings": new_findings}after_entries = [*archive_entries, new_round_entry]after = recommendations_by_topic(after_entries, threshold)
newly_crossed = []for topic, action in after.items(): if action != "mechanism": continue was_mechanism_before = before.get(topic) == "mechanism" if not was_mechanism_before: newly_crossed.append({"topic": topic, "recommended_action": action})
return newly_crossedarchive_entries = load_archive(args.archive_path)
with open(args.review_comment_path) as f: comment_text = f.read()new_findings = parse_findings_mod.parse_findings(comment_text)
if not new_findings: print("No findings in the latest review — nothing to analyze.") print("---") print(json.dumps({"newly_crossed": []}, indent=2)) return 0
newly_crossed = find_newly_crossed_topics(archive_entries, new_findings, args.threshold)
if newly_crossed: for item in newly_crossed: print( f"[{item['topic']}] newly recommends a MECHANISM-LEVEL fix " f"as of this round's findings." )else: print("No topic newly crosses the mechanism-fix threshold this round.")
print("---")print(json.dumps({"newly_crossed": newly_crossed}, indent=2))return 0for entry in entries: round_num = entry.get("round") for finding in entry.get("findings", []): topic = classify_finding(finding) if topic is None: continue topic_rounds[topic].add(round_num) if len(topic_examples[topic]) < 3: topic_examples[topic].append(f"round {round_num}: {finding[:120]}")
recommendations = []for topic, rounds in sorted(topic_rounds.items(), key=lambda kv: -len(kv[1])): recurrence = len(rounds) action = "mechanism" if recurrence >= threshold else "target" recommendations.append( { "topic": topic, "recurrence_count": recurrence, "rounds": sorted(rounds), "recommended_action": action, "examples": topic_examples[topic], } )
return {"threshold": threshold, "recommendations": recommendations}entries = load_archive(args.archive_path)result = analyze(entries, args.threshold)
for rec in result["recommendations"]: marker = "MECHANISM-LEVEL FIX RECOMMENDED" if rec["recommended_action"] == "mechanism" else "target-level fix sufficient so far" print( f"[{rec['topic']}] recurred in {rec['recurrence_count']} round(s) " f"{rec['rounds']} -> {marker}" ) for example in rec["examples"]: print(f" - {example}")
print("---")print(json.dumps(result, indent=2))return 0