Repository navigation
Fro Bot #6144
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| name: Fro Bot | |
| # Optional correlation-id suffix lets a cross-repo-dispatch caller identify | |
| # this exact run by a unique token instead of coincidental epoch/actor | |
| # matching (see scripts/cross-repo-dispatch.ts). Empty by default — a purely | |
| # cosmetic, backward-compatible run-name change. | |
| run-name: >- | |
| Fro Bot${{ github.event.inputs.correlation_id && format(' ({0})', github.event.inputs.correlation_id) || '' }} | |
| on: | |
| issue_comment: | |
| types: [created] | |
| pull_request_review_comment: | |
| types: [created] | |
| discussion_comment: | |
| types: [created] | |
| issues: | |
| types: [opened, edited] | |
| pull_request: | |
| types: [synchronize, ready_for_review, reopened, review_requested] | |
| schedule: | |
| - cron: '0 0 * * *' | |
| workflow_dispatch: | |
| inputs: | |
| prompt: | |
| description: 'Custom prompt for the Fro Bot agent (leave empty to run the default daily oversight + autohealing pass)' | |
| required: false | |
| default: '' | |
| output-mode: | |
| description: >- | |
| Delivery mode for a custom prompt: branch-pr commits and opens a PR, | |
| working-dir edits the checkout without committing. Selects which of | |
| the two daily-pass jobs runs a custom prompt; ignored when prompt is | |
| empty (the daily pass always runs both jobs in sequence). Defaults to | |
| branch-pr because a human dispatching a custom prompt is usually | |
| asking for a change to be delivered. The branch-pr job never persists | |
| wiki/knowledge updates — pick working-dir if this prompt needs that. | |
| required: false | |
| type: choice | |
| options: | |
| - branch-pr | |
| - working-dir | |
| default: branch-pr | |
| correlation_id: | |
| description: Optional opaque correlation id echoed into run-name (used by cross-repo-dispatch to identify this run) | |
| required: false | |
| default: '' | |
| workflow_call: | |
| inputs: | |
| prompt: | |
| description: Prompt passed by reusable caller workflow | |
| required: true | |
| type: string | |
| output-mode: | |
| description: >- | |
| Delivery mode this caller needs: branch-pr commits and opens a PR, | |
| working-dir edits the checkout without committing. Defaults to | |
| working-dir, the safer no-mutation choice for a caller that omits it. | |
| required: false | |
| type: string | |
| default: working-dir | |
| secrets: | |
| FRO_BOT_PAT: | |
| description: GitHub PAT for Fro Bot write-tier actions | |
| required: true | |
| OPENCODE_AUTH_JSON: | |
| description: OpenCode authentication configuration | |
| required: true | |
| OMO_PROVIDERS: | |
| description: OhMyOpenCode provider configuration | |
| required: true | |
| OPENCODE_CONFIG: | |
| description: OpenCode runtime configuration | |
| required: true | |
| GATEWAY_WEBHOOK_SECRET: | |
| description: HMAC-SHA256 signing key for the gateway presence announce (optional) | |
| required: false | |
| APPLICATION_ID: | |
| description: >- | |
| GitHub App ID used to mint installation tokens for data-branch wiki-ingest writes | |
| (optional; only needed by a caller whose output-mode can reach fro-bot-observe) | |
| required: false | |
| APPLICATION_PRIVATE_KEY: | |
| description: GitHub App private key paired with APPLICATION_ID (optional, see above) | |
| required: false | |
| permissions: | |
| contents: read | |
| # FRO_BOT_PAT carries cross-repo write-tier authority for the agent. It must | |
| # include repo access, read:org for organization queries, and project scope for | |
| # tracker Project updates when called by Gateway Rollout Tracker. | |
| concurrency: | |
| group: >- | |
| fro-bot-${{ | |
| github.event.issue.number || | |
| github.event.pull_request.number || | |
| github.event.discussion.number || | |
| ((github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && 'daily') | |
| || github.run_id | |
| }} | |
| cancel-in-progress: false | |
| env: | |
| PR_REVIEW_PROMPT: | | |
| You are a skeptical reviewer for fro-bot/.github (org control center). | |
| Focus on correctness, security, and automation safety. Prefer false positives | |
| over false negatives for security issues. | |
| Scope: | |
| - Workflow/CI changes: pin actions to full commit SHA where possible; avoid | |
| pull_request_target unless explicitly justified; prevent workflow injection | |
| (untrusted input in run: blocks or shell eval); minimal permissions; no | |
| secrets in logs. | |
| - Repo rules: no `any`; no `@ts-ignore`/`@ts-expect-error`; avoid ES6 classes; | |
| explicit function return types; use `consola` instead of `console`. | |
| - Dependencies: identify direct additions/updates; flag known advisories; | |
| avoid unnecessary or duplicate deps; ensure license compatibility. | |
| - Tests: verify new/changed behavior has tests covering happy path, errors, | |
| and boundaries. If tests aren't needed, explain why. | |
| Hard boundaries: | |
| - Review only. Do NOT push commits, modify files, create branches, or open PRs. | |
| - Do NOT use labels, assignments, or milestone changes. | |
| - Use inline comments for file-specific findings. Use review body for summary only. | |
| - Do NOT speculate. If uncertain, state what evidence is missing. | |
| Do NOT comment on formatting, lint, or style nits handled by CI. | |
| Output: | |
| ## Verdict: PASS / CONDITIONAL / REJECT | |
| ### Blocking issues | |
| ### Non-blocking concerns | |
| ### Missing tests | |
| ### Risk assessment (LOW/MED/HIGH) + rationale | |
| Requirements: | |
| - Include every heading exactly once and in the order above. | |
| - Write "None" under headings with no findings. | |
| - Keep findings actionable: file path, impact, and concrete remediation. | |
| GOAL_DECOMPOSITION_PROMPT: | | |
| This issue carries the `cross-repo-goal` label: it describes a goal spanning | |
| multiple owner repos (`fro-bot/*`, `marcusrbrown/*`). Propose a per-repo | |
| work-item decomposition as a single comment on this issue. Do NOT dispatch, | |
| trigger, or execute anything — this run has no dispatch capability and this | |
| output is a proposal only, subject to the operator's edit and approval. | |
| Read the goal description and the managed-repo registry | |
| (`metadata/repos.yaml`) to identify eligible owner-repo targets. | |
| Post exactly one checklist item per proposed work item, in this literal | |
| format (each line independently parseable): | |
| - [ ] <owner>/<name>: <prompt> | |
| Wrap the checklist in a delimited region so the parser can find it | |
| precisely even when you also write prose around it: | |
| <!-- fro-bot:cross-repo-items:start --> | |
| - [ ] <owner>/<name>: <prompt> | |
| <!-- fro-bot:cross-repo-items:end --> | |
| Rules: | |
| - <owner>/<name> must be an owner-repo target (`fro-bot/*` or | |
| `marcusrbrown/*`) present in `metadata/repos.yaml`. | |
| - <prompt> is the exact task to hand to that repo's worker run — concise, | |
| concrete, self-contained. | |
| - Free text outside the delimited region is fine — an intro, an | |
| explanation of why each target was chosen, a footer — as long as it | |
| doesn't itself look like a checklist item. | |
| - If the goal cannot be decomposed into owner-repo targets, say so plainly | |
| instead of inventing items. | |
| Downstream, once an item is approved and dispatched, the target repo's | |
| worker run receives a prompt carrying this item's work plus a receipt | |
| contract. That contract is generated automatically — you do not need to | |
| restate it in <prompt> — but it is documented here so the shape of the | |
| full loop is clear: | |
| A dispatched worker must, after doing the item's work, post a completion | |
| receipt as a comment on THIS coordination issue, wrapped in its own | |
| delimited region: | |
| <!-- fro-bot:cross-repo-result:start --> | |
| <!-- fro-bot:cross-repo-result {"correlation_id":"...","nonce":"...","status":"success","summary":"...","pr":"https://github.com/owner/repo/pull/123"} --> | |
| <!-- fro-bot:cross-repo-result:end --> | |
| Receipt rules the worker follows: | |
| - `status` is exactly one of `success`, `noop`, or `failed` — nothing else. | |
| - A receipt is MANDATORY even when the outcome is a no-op; a missing | |
| receipt reads as a failure to report, not as success. | |
| - Before finishing, the worker echoes the coordination issue it resolved | |
| (owner/repo/number) and re-reads its own posted comment to confirm the | |
| marker round-trips; if the marker is missing, it edits the comment to | |
| add it before declaring the item done. | |
| - `summary` must be plain text: no markdown formatting, backticks, or | |
| backslash escapes, and the marker as a whole must be valid JSON — an | |
| escaped character like `\#` breaks the parser and turns a successful | |
| run into an unparseable receipt. | |
| # ── Daily pass prompt pieces ──────────────────────────────────────────── | |
| # SCHEDULE_PROMPT used to be one monolithic block covering categories 1–8. | |
| # It is now split across two jobs (fro-bot-remediate: categories 1–4 via | |
| # branch-pr; fro-bot-observe: categories 5–8 + the daily report via | |
| # working-dir). Sections that must stay identical in both prompts live in | |
| # SHARED_RULES / HARD_BOUNDARIES / AGENT_NOTES below and are concatenated | |
| # at the point of use with format() — GitHub Actions env: entries cannot | |
| # reference each other, but a step's env: block can reference workflow-level | |
| # env, so the concatenation happens in each job's "Run Fro Bot" step. | |
| SHARED_RULES: | | |
| For version comparisons, current-version truth comes from the authoritative | |
| registry or upstream release source; name that source and state whether major | |
| drift was included. | |
| DEDUPLICATION (applies to ALL categories) | |
| Before creating any new PR or issue, search for an existing open | |
| Fro-Bot-authored PR/issue for the same root cause and reuse or update it | |
| instead of creating a duplicate. | |
| SCOPE CAP | |
| Only auto-heal a fix that is clearly minimal and reversible. If the smallest | |
| safe fix is a broad refactor, an architecture change, or a many-file edit, | |
| do NOT auto-heal it — record it under "Needs Human Attention" with enough | |
| detail for an LLM agent to act on later (see AGENT NOTES). | |
| DEPENDENCY OWNERSHIP | |
| Renovate owns routine dependency and version bumps. Change dependency | |
| versions only to remediate a confirmed critical/high security advisory or to | |
| repair an existing security-update PR. Never open a non-security version-bump | |
| PR and never batch unrelated dependency changes into a security fix. | |
| TRUSTED AUTHORS | |
| Only repair a PR branch authored by the repository owner/a collaborator with | |
| write access, or an approved automation bot (renovate[bot], dependabot[bot], | |
| fro-bot). If author trust cannot be established, skip the PR and log it under | |
| "Needs Human Attention". | |
| HARD_BOUNDARIES: | | |
| HARD BOUNDARIES | |
| - Never force-push, rewrite history, delete branches, merge PRs, submit | |
| approvals, or close/reopen PRs. | |
| - Never push directly to main. Direct pushes are allowed only to an existing | |
| non-default PR branch you are repairing under category 1 or 2. | |
| - Never modify secrets, org settings, environments, or branch protection. | |
| - Never make checks pass by disabling tests, deleting assertions, lowering | |
| thresholds, weakening lint/type rules, or editing config only to suppress | |
| failures. If the smallest safe fix would weaken a guardrail, skip and log. | |
| - Do not modify .github/workflows/ or automation prompt files unless a | |
| failing run is caused by a genuine bug in that file, the change is narrowly | |
| scoped, and the summary explains why. | |
| AGENT_NOTES: | | |
| AGENT NOTES (instead of per-agent follow-up tasks) | |
| Do NOT create follow-up tasks addressed to a specific agent (Fro Bot, | |
| Copilot, etc.). When work is deferred, write the note so any LLM agent can | |
| pick it up cold: state the exact file paths, the root cause, the smallest | |
| safe fix, any constraints or "do not retry" warnings, and how to verify. | |
| Put these under "Needs Human Attention" in the report. If a finding is | |
| durable repository knowledge, also persist it to the wiki per the | |
| knowledge_persistence block, following knowledge/schema.md. | |
| REMEDIATE_INTRO: | | |
| Run the remediation half of the daily Fro Bot pass for fro-bot/.github — the | |
| autonomous control center for the fro-bot GitHub account. This run handles | |
| categories 1–4 (reactive autohealing): find what is broken and fix it. | |
| Categories 5–8 (proactive oversight, report-only) and the daily report run | |
| afterward in a separate pass and are out of scope here. Prioritize minimal, | |
| reversible changes. Read the canonical context first: README.md, | |
| .github/copilot-instructions.md, and AGENTS.md if present. | |
| EXECUTION MODEL | |
| This run delivers in branch-pr mode: every fix below lands by checking out a | |
| branch, committing, pushing, and opening or updating a PR — that is the | |
| required delivery mechanism here, not a fallback. Analyze every category, but | |
| perform write actions serially. Never keep more than one branch checked out | |
| at a time — complete one mutation (commit, push, PR), return to a clean | |
| working tree, then continue to the next fix. | |
| Never commit to knowledge/wiki/**, knowledge/index.md, knowledge/log.md, | |
| knowledge/corrections.yaml, or metadata/*.yaml and metadata/*.yml from this run, and never open | |
| a PR touching those paths — Check Wiki Authority rejects them against main | |
| regardless. The control plane's data branch is the authoritative writer for | |
| knowledge/** and metadata/**, written only through the established | |
| wiki/metadata scripts. If a fix would require touching those paths, log it | |
| under "Needs Human Attention" instead. | |
| REMEDIATE_CATEGORIES: | | |
| CATEGORIES (work in order) | |
| 1. ERRORED PRs | |
| Find open PRs with failing CI. Skip dependency/security PRs here (handle | |
| in category 2). Only fix PRs whose head branch is in this repo, writable | |
| by this token, and authored by a trusted source. For each fixable PR: | |
| inspect both sources: check runs and legacy commit statuses. `gh pr checks` | |
| and `/commits/{sha}/check-runs` enumerate check runs only; legacy statuses | |
| are separate `/commits/{sha}/status` records, so inspect both and report | |
| failures from either source under "Errored PRs". | |
| check out the branch, read the failing check logs, diagnose the root | |
| cause, apply a focused fix, push to that PR branch, and post a concise PR | |
| comment (root cause, fix applied, remaining risk). If the PR touches | |
| .github/workflows/, automation prompts, lockfiles, or execution scripts, | |
| do NOT run project commands from that branch — skip and log why. | |
| 2. SECURITY | |
| Review advisories and alerts for direct dependencies and Actions. If a | |
| security PR is failing or conflicted, fix and push to it. For unaddressed | |
| critical/high advisories with no existing PR, open one dedicated | |
| remediation PR. Never bundle unrelated upgrades. If advisory data is | |
| unavailable to the token, note "security alerts unavailable" under "Needs | |
| Human Attention" — do not guess. | |
| 3. CONTROL-PLANE INTEGRITY (highest-value for this repo) | |
| a. SHA pinning: every third-party action in .github/workflows/*.yaml and | |
| .github/actions/**/action.yaml must be pinned to a full commit SHA with | |
| a version comment (@<sha> # vX.Y.Z). Fix floating tags on a PR. | |
| b. Strip-only TypeScript: scripts/*.ts must avoid enum, namespace, | |
| parameter properties, and TS import aliases (Node 24 strip-only). The | |
| eslint erasable-syntax rule and the Test Scripts Load job enforce this; | |
| flag any drift. | |
| c. Least privilege: workflows should declare minimal permissions and use | |
| the shared ./.github/actions/setup. Flag over-broad permissions. | |
| d. Guard integrity: do not weaken the wiki-authority guard, the privacy | |
| gates, or branch protection. Report gaps; never relax them. | |
| 4. CODE QUALITY | |
| Run the repository's own validation: pnpm bootstrap, pnpm check-types, | |
| pnpm lint, pnpm test. Apply safe mechanical fixes (lint/format autofixes) | |
| on a PR branch — never commit directly to main. If a check fails and the | |
| fix is not minimal/reversible, open or update an issue and log it. | |
| OBSERVE_INTRO: | | |
| Run the oversight half of the daily Fro Bot pass for fro-bot/.github — the | |
| autonomous control center for the fro-bot GitHub account. This run handles | |
| categories 5–8 (proactive oversight, report-only) and writes the daily | |
| report. A separate remediation pass already ran categories 1–4 (reactive | |
| autohealing) via branch+PR before this run started. Read the canonical | |
| context first: README.md, .github/copilot-instructions.md, and AGENTS.md if | |
| present. | |
| EXECUTION MODEL | |
| This run delivers in working-dir mode: never run `git branch`, `git commit`, | |
| `git push`, `gh pr create`, `gh pr merge`, create or switch branches, or use | |
| any tool/skill that delivers via branch+PR. All observable output goes into | |
| issue/PR comments and the daily report issue created below. Never write | |
| metadata/*.yaml from this run — that path is owned exclusively by the data | |
| branch via commit-metadata.ts. | |
| The one exception is durable knowledge capture: if this pass surfaces | |
| knowledge worth persisting, write it directly into knowledge/wiki/**, | |
| knowledge/index.md, and knowledge/log.md in the working tree per | |
| knowledge/schema.md, and leave that tree dirty when you finish this run. | |
| `wiki-ingest.ts` discovers its payload by running `git status --porcelain` | |
| and reading whatever it finds changed; a clean tree means ingestion silently | |
| delivers nothing and exits 0. Do not run `git checkout`, `git restore`, or | |
| any other cleanup against knowledge/** before finishing — a later step in | |
| this same job captures and commits that diff for you. Do not commit those | |
| changes yourself. | |
| OBSERVE_CATEGORIES: | | |
| CATEGORIES (work in order) | |
| 5. OVERSIGHT (org-wide detection, report-only) | |
| Scope: all repositories the fro-bot account can see. Enumerate them with | |
| paginated repository listing API calls for the authenticated account and its | |
| organizations; define "can see" as repositories returned with at least read | |
| access, and report anything that could not be enumerated. Summarize with | |
| links only. Detect: new issues, open PRs, stale issues (>30d) and PRs (>7d | |
| aging, >14d stale), failing main checks, security alerts, unassigned bugs, | |
| and the top three repo hotspots (ranked by qualifying finding count in the | |
| current daily snapshot; at least one linked finding required). For PRs, | |
| measure aging from creation time and staleness from the most recent update | |
| activity (commit, review, comment, or state change). Recommend a next step | |
| for each. Do NOT modify individual issues/PRs or apply labels in this | |
| category. | |
| 6. CROSS-PROJECT INTELLIGENCE (report-only) | |
| Fro Bot surveys tracked repositories into the wiki. Treat every entry in | |
| metadata/repos.yaml (not an example list) as tracked; scan every accessible | |
| entry for automation patterns, prompt strategies, or workflow improvements | |
| that could benefit this repository. Report inaccessible or otherwise | |
| unscanned entries and mark coverage partial. Report adoptable findings only; | |
| make no changes in this category. | |
| 7. PROGRESSIVE IMPROVEMENT (report-only) | |
| Scan for durable improvement opportunities: tool-version drift in | |
| package.json (ESLint, Prettier, TypeScript, Vitest more than a minor | |
| behind), missing or degraded CI jobs, convention drift from | |
| copilot-instructions.md, and stale TODO/FIXME annotations. Also scan | |
| open issues labeled `learning-proposal`: the expected action for each | |
| is authoring the learning into `docs/solutions/`, so any older than 14 | |
| days, or two or more open at once, means the compounding pipeline has | |
| stalled. Report the count and ages. Do not treat a `healthy` reading on | |
| the Improvement Metrics report (#3674) as evidence this is fine — | |
| unauthored proposals never become classes there, so that report stays | |
| green precisely while this is stalled. Report under "Progressive | |
| Improvement"; make no changes here. | |
| 8. GATEWAY OPERATOR CONTROL-SURFACE ROLLOUT TRACKER | |
| Review awareness only. Compare #3512's checklist/status claims with the | |
| corresponding GitHub Project 1 item status and the latest rollout evidence | |
| in managed repositories; report any mismatch with both values. Summarize | |
| drift in the Daily Org Oversight Report if found. Do not post tracker | |
| comments or edit Project fields from this daily report path; the dedicated | |
| Gateway Rollout Tracker workflow owns tracker writes. | |
| OBSERVE_OUTPUT: | | |
| OUTPUT — ONE PERPETUAL SUMMARY ISSUE | |
| This report still covers categories 1–4, but this job did not do that work — | |
| a separate remediation pass already ran them in branch-pr mode before this | |
| job started. Populate the Errored PRs, Security, Control-Plane Integrity, and | |
| Code Quality sections and their Run Summary rows by inspecting what that pass | |
| produced: open Fro-Bot-authored PRs relevant to each category, their current | |
| check status, and any comments left on them. Do not re-analyze those | |
| categories from scratch. If you cannot find evidence of what the remediation | |
| pass did for a category, mark it ❔ and say so. | |
| Maintain exactly one daily report. Create an issue titled | |
| "Daily Fro Bot Report — YYYY-MM-DD (UTC)" for today. Before or after | |
| creating it, CLOSE every older open issue whose title starts with | |
| "Daily Fro Bot Report —" or "Daily Org Oversight Report —" (the previous | |
| naming), leaving a one-line closing comment that links the newest report. | |
| There must be exactly one open daily report when the run finishes. | |
| The issue body MUST use this structure (write "None" for empty sections, | |
| links only — no full content duplication): | |
| ## Daily Fro Bot Report — YYYY-MM-DD (UTC) | |
| ### Run Summary | |
| | Category | Status | Notes | | |
| | --- | --- | --- | | |
| | Errored PRs | ✅/⚠️/❌/❔ | ... | | |
| | Security | ✅/⚠️/❌/❔ | ... | | |
| | Control-Plane Integrity | ✅/⚠️/❌/❔ | ... | | |
| | Code Quality | ✅/⚠️/❌/❔ | ... | | |
| | Oversight | ✅/⚠️/❌/❔ | ... | | |
| | Cross-Project Intelligence | ✅/⚠️/❌/❔ | ... | | |
| | Progressive Improvement | ✅/⚠️/❌/❔ | ... | | |
| ### Errored PRs | |
| ### Security | |
| ### Control-Plane Integrity | |
| ### Code Quality | |
| ### Oversight | |
| ### Cross-Project Intelligence | |
| ### Progressive Improvement | |
| ### Needs Human Attention | |
| Status legend: ✅ = verified-clean; ⚠️/❌ = warning or error; ❔ = | |
| could-not-check. Use ✅ only when all required sources were checked and | |
| nothing was found. Use ❔ when any source is unavailable, stale, empty, | |
| access denied, or scan failed; name the affected source and reason. Never | |
| render ✅ for could-not-check. | |
| jobs: | |
| # ── Content-triggered job: comments, issues, PR review, cross-repo-goal ── | |
| # output-mode is ignored for these events (the agent only branches this on | |
| # schedule/workflow_dispatch), so this job is untouched by the delivery-mode | |
| # split below. | |
| fro-bot: | |
| name: Fro Bot | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| outputs: | |
| wiki-changed: ${{ steps.wiki-changes.outputs.changed }} | |
| if: >- | |
| ( | |
| github.event.pull_request == null || | |
| ( | |
| !github.event.pull_request.head.repo.fork && | |
| !endsWith(github.event.pull_request.user.login || '', '[bot]') | |
| ) | |
| ) && ( | |
| ( | |
| github.event_name == 'issues' && | |
| !endsWith(github.event.issue.user.login || '', '[bot]') && | |
| (github.event.issue.user.login || '') != 'fro-bot' && | |
| contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.issue.author_association || '') | |
| ) || | |
| ( | |
| github.event_name == 'pull_request' && | |
| !endsWith(github.event.pull_request.user.login || '', '[bot]') && | |
| (github.event.pull_request.user.login || '') != 'fro-bot' | |
| ) || | |
| ( | |
| (github.event_name == 'issue_comment' || | |
| github.event_name == 'pull_request_review_comment' || | |
| github.event_name == 'discussion_comment') && | |
| contains(github.event.comment.body || '', '@fro-bot') && | |
| (github.event.comment.user.login || '') != 'fro-bot' && | |
| contains(fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'), github.event.comment.author_association || '') | |
| ) | |
| ) | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.FRO_BOT_PAT }} | |
| # Content-triggered runs never retain checkout credentials. | |
| persist-credentials: false | |
| - name: 📦 Setup | |
| uses: ./.github/actions/setup | |
| - name: Sync wiki from data branch | |
| uses: ./.github/actions/sync-wiki | |
| - name: Capture wiki baseline | |
| id: wiki-baseline | |
| run: | | |
| git diff --no-ext-diff -- knowledge/index.md knowledge/log.md knowledge/wiki > "$RUNNER_TEMP/wiki-baseline.diff" | |
| echo "hash=$(shasum -a 256 "$RUNNER_TEMP/wiki-baseline.diff" | cut -d ' ' -f1)" >> "$GITHUB_OUTPUT" | |
| # Content-hash snapshot of every wiki-scoped path as it stands right after | |
| # sync-wiki, before the agent runs. wiki-handoff-build.ts diffs against this so | |
| # the handoff only carries what the agent actually changed — not every path | |
| # `data` differs from `main` on. See scripts/wiki-handoff-core.ts. | |
| - name: Capture wiki content baseline | |
| env: | |
| WIKI_HANDOFF_BASELINE_PATH: ${{ runner.temp }}/wiki-content-baseline-fro-bot.json | |
| run: node scripts/wiki-handoff-baseline.ts | |
| - name: 🎭 Load Persona | |
| id: persona | |
| run: | | |
| delimiter="EOF_$(openssl rand -hex 8)" | |
| { | |
| echo "content<<$delimiter" | |
| cat persona/fro-bot-persona.md | |
| echo "$delimiter" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Query wiki context | |
| id: wiki-query | |
| env: | |
| WIKI_QUERY_EVENT_NAME: ${{ github.event_name }} | |
| WIKI_QUERY_OWNER: ${{ github.repository_owner }} | |
| WIKI_QUERY_REPO: ${{ github.event.repository.name }} | |
| WIKI_QUERY_TITLE: >- | |
| ${{ | |
| github.event.pull_request.title || | |
| github.event.issue.title || | |
| github.event.discussion.title || | |
| '' | |
| }} | |
| WIKI_QUERY_BODY: >- | |
| ${{ | |
| github.event.pull_request.body || | |
| github.event.issue.body || | |
| github.event.comment.body || | |
| '' | |
| }} | |
| # Run-local handoff for optional wiki context expansion. | |
| # Contains only selected wiki paths — never logged, never echoed. | |
| WIKI_CONTEXT_HANDOFF_PATH: ${{ runner.temp }}/wiki-context-handoff-${{ github.run_id }}-${{ github.run_attempt }}.json | |
| run: node scripts/wiki-query.ts | |
| - name: Query solutions context | |
| id: solutions-query | |
| env: | |
| SOLUTIONS_QUERY_EVENT_NAME: ${{ github.event_name }} | |
| SOLUTIONS_QUERY_OWNER: ${{ github.repository_owner }} | |
| SOLUTIONS_QUERY_REPO: ${{ github.event.repository.name }} | |
| SOLUTIONS_QUERY_TITLE: >- | |
| ${{ | |
| github.event.pull_request.title || | |
| github.event.issue.title || | |
| github.event.discussion.title || | |
| '' | |
| }} | |
| SOLUTIONS_QUERY_BODY: >- | |
| ${{ | |
| github.event.pull_request.body || | |
| github.event.issue.body || | |
| github.event.comment.body || | |
| '' | |
| }} | |
| run: node scripts/solutions-query.ts | |
| - name: Run Fro Bot | |
| id: fro-bot-agent | |
| uses: fro-bot/agent@e6efc1f13ed05056cc9ba68d6f8fb5e71bed8ca6 # v0.117.0 | |
| env: | |
| OPENCODE_PROMPT_ARTIFACT: 'true' | |
| PERSONA: ${{ steps.persona.outputs.content }} | |
| WIKI_CONTEXT: ${{ steps.wiki-query.outputs.excerpt }} | |
| SOLUTIONS_CONTEXT: ${{ steps.solutions-query.outputs.excerpt }} | |
| # Handoff path only — never the file contents or selected paths themselves. | |
| WIKI_CONTEXT_HANDOFF_PATH: ${{ runner.temp }}/wiki-context-handoff-${{ github.run_id }}-${{ github.run_attempt }}.json | |
| TASK_PROMPT: >- | |
| ${{ | |
| (github.event_name == 'pull_request' && env.PR_REVIEW_PROMPT) | |
| || (github.event_name == 'issues' && contains(github.event.issue.labels.*.name, 'cross-repo-goal') && env.GOAL_DECOMPOSITION_PROMPT) | |
| || '' | |
| }} | |
| with: | |
| github-token: ${{ secrets.FRO_BOT_PAT }} | |
| auth-json: ${{ secrets.OPENCODE_AUTH_JSON }} | |
| model: ${{ vars.FRO_BOT_MODEL }} | |
| omo-providers: ${{ secrets.OMO_PROVIDERS }} | |
| opencode-config: ${{ secrets.OPENCODE_CONFIG }} | |
| prompt: | | |
| <persona> | |
| ${{ env.PERSONA }} | |
| </persona> | |
| ${{ env.TASK_PROMPT }} | |
| <wiki_context> | |
| ${{ env.WIKI_CONTEXT }} | |
| </wiki_context> | |
| <solutions_context> | |
| ${{ env.SOLUTIONS_CONTEXT }} | |
| </solutions_context> | |
| <wiki_context_expansion> | |
| The wiki context above is baseline-injected and usually enough on its own — do not | |
| expand by default. If baseline context is insufficient, following linked pages would | |
| help, or you have a short grounded query, you may optionally run one of: | |
| `node scripts/wiki-context-expand.ts linked` | |
| `node scripts/wiki-context-expand.ts query "<short grounded query>"` | |
| Linked mode reads the baseline-selected pages from the path in | |
| `WIKI_CONTEXT_HANDOFF_PATH`; do not print or echo that path, its file contents, the | |
| query text, returned excerpts, or any token/env values into your output or logs. Use | |
| returned wiki paths only to ground a recommendation. | |
| </wiki_context_expansion> | |
| <knowledge_persistence> | |
| Use the wiki context when it materially improves the response. | |
| Consult the solutions context for prior learnings relevant to this work; | |
| when one applies, follow it and cite its `Path:` in your reasoning. Treat | |
| entries marked as candidate suggestions as hints to verify, not settled facts. | |
| If this interaction surfaces durable knowledge worth keeping, update only | |
| `knowledge/wiki/**`, `knowledge/index.md`, and `knowledge/log.md` following | |
| `knowledge/schema.md`. Keep changes additive, preserve contradictions with | |
| dates and sources, and leave the wiki untouched when the signal is weak. | |
| </knowledge_persistence> | |
| - name: Detect wiki insight changes | |
| id: wiki-changes | |
| if: always() && !cancelled() | |
| run: | | |
| git diff --no-ext-diff -- knowledge/index.md knowledge/log.md knowledge/wiki > "$RUNNER_TEMP/wiki-current.diff" | |
| current_hash=$(shasum -a 256 "$RUNNER_TEMP/wiki-current.diff" | cut -d ' ' -f1) | |
| if [ "$current_hash" = "${{ steps.wiki-baseline.outputs.hash }}" ]; then | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Build a data-only handoff artifact from the working tree instead of writing `data` | |
| # from this job directly — this job just ran the LLM agent, whose shell can rewrite | |
| # any file in the checkout (including this script). The artifact carries only | |
| # manifest.json + changed files, no ingest metadata: WIKI_OPERATION/WIKI_TARGET/etc. | |
| # are set from trusted github.* context directly in the trusted job below, never | |
| # from anything this job could have produced. See scripts/wiki-handoff-core.ts. | |
| - name: Build wiki handoff artifact | |
| id: wiki-handoff-build | |
| if: >- | |
| success() && | |
| steps.wiki-changes.outputs.changed == 'true' && | |
| contains(fromJSON('["issue_comment","pull_request_review_comment","discussion_comment","issues","pull_request"]'), github.event_name) | |
| env: | |
| WIKI_HANDOFF_DIR: ${{ runner.temp }}/wiki-handoff-fro-bot | |
| WIKI_HANDOFF_BASELINE_PATH: ${{ runner.temp }}/wiki-content-baseline-fro-bot.json | |
| run: node scripts/wiki-handoff-build.ts | |
| # Baseline scoping (see the build step's env) can legitimately narrow a manifest | |
| # to empty even when the outer wiki-changes diff-hash flagged something — e.g. an | |
| # edit reverted before the run ended. An empty manifest must mean no artifact and | |
| # no downstream ingest, matching the pre-scoping no-op contract exactly. | |
| - name: Upload wiki handoff artifact | |
| if: steps.wiki-handoff-build.outcome == 'success' && steps.wiki-handoff-build.outputs.changed == 'true' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: wiki-handoff-fro-bot-${{ github.run_id }} | |
| path: ${{ runner.temp }}/wiki-handoff-fro-bot | |
| retention-days: 1 | |
| # ── Trusted writer: applies the fro-bot job's wiki handoff artifact to `data` ──────── | |
| # Fresh default-branch checkout, no agent step — the only job in this workflow allowed | |
| # to run wiki-ingest.ts with an App write token. See fro-bot.yaml's Group 1 migration | |
| # note in docs/plans and scripts/wiki-handoff-core.ts. | |
| fro-bot-wiki-ingest: | |
| name: Fro Bot — Ingest wiki insight | |
| needs: fro-bot | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| if: needs.fro-bot.outputs.wiki-changed == 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout trusted code (default branch) | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| persist-credentials: false | |
| - name: 📦 Setup | |
| uses: ./.github/actions/setup | |
| # Recomputed here (not reused from the agent job) so the ingest metadata is entirely | |
| # trusted-job-sourced: github.* context plus this job's own clock. | |
| - name: Capture ingest timestamp | |
| id: ingest-ts | |
| run: echo "now=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" | |
| - name: Sync wiki from data branch | |
| uses: ./.github/actions/sync-wiki | |
| - name: Download wiki handoff artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: wiki-handoff-fro-bot-${{ github.run_id }} | |
| path: ${{ runner.temp }}/wiki-handoff | |
| - name: Validate and apply wiki handoff | |
| env: | |
| WIKI_HANDOFF_DIR: ${{ runner.temp }}/wiki-handoff | |
| run: node scripts/wiki-handoff-apply.ts | |
| - name: 🔑 Mint App token for wiki ingest | |
| id: app-token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| app-id: ${{ secrets.APPLICATION_ID }} | |
| private-key: ${{ secrets.APPLICATION_PRIVATE_KEY }} | |
| repositories: ${{ github.event.repository.name }} | |
| permission-contents: write | |
| - name: Ingest wiki insight changes | |
| env: | |
| GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} | |
| WIKI_OPERATION: event | |
| WIKI_TARGET: 'repo:${{ github.repository }}' | |
| WIKI_SUMMARY: Persisted durable knowledge from the ${{ github.event_name }} interaction on ${{ github.repository }}. | |
| WIKI_COMMIT_MESSAGE: 'feat(knowledge): ingest ${{ github.event_name }} context for ${{ github.repository }}' | |
| WIKI_SOURCES: '[{"url":"https://github.com/${{ github.repository }}","sha":"${{ github.sha }}","accessed":"${{ steps.ingest-ts.outputs.now }}"}]' | |
| run: | | |
| warning_file="$RUNNER_TEMP/wiki-ingest-warnings" | |
| if node scripts/wiki-ingest.ts 2>"$warning_file"; then | |
| status=0 | |
| else | |
| status=$? | |
| fi | |
| cat "$warning_file" >&2 | |
| if grep -q '^wiki-ingest:warning:' "$warning_file"; then | |
| { | |
| echo '### Wiki ingest warnings' | |
| grep '^wiki-ingest:warning:' "$warning_file" | sed 's/^wiki-ingest:warning:/- /' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| exit "$status" | |
| # ── Remediation half of the daily pass: categories 1–4, branch-pr ──────── | |
| # Runs for the scheduled cron, an empty-prompt workflow_dispatch (daily | |
| # pass — both jobs run), or a custom prompt whose output-mode resolves to | |
| # branch-pr (a direct workflow_dispatch defaulting to branch-pr, or a | |
| # workflow_call caller like apply-branding.yaml that requests it). | |
| fro-bot-remediate: | |
| name: Fro Bot — Remediate | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| if: >- | |
| (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && | |
| (inputs.prompt == '' || inputs['output-mode'] == 'branch-pr') | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.FRO_BOT_PAT }} | |
| - name: 📦 Setup | |
| uses: ./.github/actions/setup | |
| - name: Sync wiki from data branch | |
| uses: ./.github/actions/sync-wiki | |
| - name: 🎭 Load Persona | |
| id: persona | |
| run: | | |
| delimiter="EOF_$(openssl rand -hex 8)" | |
| { | |
| echo "content<<$delimiter" | |
| cat persona/fro-bot-persona.md | |
| echo "$delimiter" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Query wiki context | |
| id: wiki-query | |
| env: | |
| WIKI_QUERY_EVENT_NAME: ${{ github.event_name }} | |
| WIKI_QUERY_OWNER: ${{ github.repository_owner }} | |
| WIKI_QUERY_REPO: ${{ github.event.repository.name }} | |
| WIKI_QUERY_TITLE: '' | |
| WIKI_QUERY_BODY: '' | |
| # Run-local handoff for optional wiki context expansion. | |
| # Contains only selected wiki paths — never logged, never echoed. | |
| WIKI_CONTEXT_HANDOFF_PATH: ${{ runner.temp }}/wiki-context-handoff-${{ github.run_id }}-${{ github.run_attempt }}-remediate.json | |
| run: node scripts/wiki-query.ts | |
| - name: Query solutions context | |
| id: solutions-query | |
| env: | |
| SOLUTIONS_QUERY_EVENT_NAME: ${{ github.event_name }} | |
| SOLUTIONS_QUERY_OWNER: ${{ github.repository_owner }} | |
| SOLUTIONS_QUERY_REPO: ${{ github.event.repository.name }} | |
| SOLUTIONS_QUERY_TITLE: '' | |
| SOLUTIONS_QUERY_BODY: '' | |
| run: node scripts/solutions-query.ts | |
| # Sync wiki from data branch (above) intentionally dirties knowledge/** | |
| # relative to HEAD so wiki-query.ts reads the freshest content. This job | |
| # delivers via branch-pr with no baseline/detect step to separate that | |
| # pre-existing dirt from an agent edit, so a blanket `git add -A` or | |
| # `git commit -a` would sweep the whole data-branch delta into every | |
| # remediation PR — which Check Wiki Authority then rejects. Now that | |
| # wiki-query.ts has consumed the synced content, restore knowledge/** to | |
| # HEAD before the agent runs so there is nothing dirty left to sweep in. | |
| - name: Restore knowledge/ to a clean tree before the agent runs | |
| run: | | |
| git restore --worktree --source=HEAD -- knowledge | |
| # restore only manages tracked paths, so a page that exists on `data` but not | |
| # on `main` lands untracked and survives it. -fd, not -fdx: nothing under | |
| # knowledge/ is gitignored, and the pathspec keeps the scope to what the sync wrote. | |
| git clean -fd -- knowledge | |
| if [ -n "$(git status --porcelain -- knowledge)" ]; then | |
| echo "::error::knowledge/ is still dirty after restore — remediate must start clean" >&2 | |
| git status --porcelain -- knowledge >&2 | |
| exit 1 | |
| fi | |
| - name: Run Fro Bot | |
| id: fro-bot-agent | |
| uses: fro-bot/agent@e6efc1f13ed05056cc9ba68d6f8fb5e71bed8ca6 # v0.117.0 | |
| env: | |
| OPENCODE_PROMPT_ARTIFACT: 'true' | |
| PERSONA: ${{ steps.persona.outputs.content }} | |
| WIKI_CONTEXT: ${{ steps.wiki-query.outputs.excerpt }} | |
| SOLUTIONS_CONTEXT: ${{ steps.solutions-query.outputs.excerpt }} | |
| WIKI_CONTEXT_HANDOFF_PATH: ${{ runner.temp }}/wiki-context-handoff-${{ github.run_id }}-${{ github.run_attempt }}-remediate.json | |
| TASK_PROMPT: >- | |
| ${{ | |
| (github.event_name == 'workflow_dispatch' && github.event.inputs.prompt) | |
| || inputs.prompt | |
| || format('{0}{1}{2}{3}{4}', env.REMEDIATE_INTRO, env.SHARED_RULES, env.REMEDIATE_CATEGORIES, env.HARD_BOUNDARIES, env.AGENT_NOTES) | |
| }} | |
| with: | |
| github-token: ${{ secrets.FRO_BOT_PAT }} | |
| auth-json: ${{ secrets.OPENCODE_AUTH_JSON }} | |
| model: ${{ vars.FRO_BOT_MODEL }} | |
| omo-providers: ${{ secrets.OMO_PROVIDERS }} | |
| opencode-config: ${{ secrets.OPENCODE_CONFIG }} | |
| output-mode: branch-pr | |
| prompt: | | |
| <persona> | |
| ${{ env.PERSONA }} | |
| </persona> | |
| ${{ env.TASK_PROMPT }} | |
| <wiki_context> | |
| ${{ env.WIKI_CONTEXT }} | |
| </wiki_context> | |
| <solutions_context> | |
| ${{ env.SOLUTIONS_CONTEXT }} | |
| </solutions_context> | |
| <wiki_context_expansion> | |
| The wiki context above is baseline-injected and usually enough on its own — do not | |
| expand by default. If baseline context is insufficient, following linked pages would | |
| help, or you have a short grounded query, you may optionally run one of: | |
| `node scripts/wiki-context-expand.ts linked` | |
| `node scripts/wiki-context-expand.ts query "<short grounded query>"` | |
| Linked mode reads the baseline-selected pages from the path in | |
| `WIKI_CONTEXT_HANDOFF_PATH`; do not print or echo that path, its file contents, the | |
| query text, returned excerpts, or any token/env values into your output or logs. Use | |
| returned wiki paths only to ground a recommendation. | |
| </wiki_context_expansion> | |
| <knowledge_persistence> | |
| Use the wiki context when it materially improves the response. | |
| Consult the solutions context for prior learnings relevant to this work; | |
| when one applies, follow it and cite its `Path:` in your reasoning. Treat | |
| entries marked as candidate suggestions as hints to verify, not settled facts. | |
| This job delivers via branch-pr and must not write knowledge/** or metadata/** | |
| (see EXECUTION MODEL above). Wiki persistence does not happen in this run at | |
| all — there is no later step in this job that captures or commits a wiki edit, | |
| so if this interaction surfaces durable knowledge worth keeping, say so under | |
| "Needs Human Attention" instead of writing it; a run dispatched with | |
| output-mode: working-dir is the one that can actually persist it. | |
| </knowledge_persistence> | |
| # ── Oversight half of the daily pass: categories 5–8 + daily report ────── | |
| # working-dir. Runs after fro-bot-remediate (whether it ran or was | |
| # skipped) so the report can describe remediation that actually landed. | |
| # Also the single job for a custom prompt whose output-mode resolves to | |
| # working-dir (a workflow_dispatch override, or a workflow_call caller | |
| # like gateway-rollout-tracker.yaml that relies on the working-dir default). | |
| fro-bot-observe: | |
| name: Fro Bot — Observe | |
| needs: fro-bot-remediate | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| outputs: | |
| wiki-changed: ${{ steps.wiki-changes.outputs.changed }} | |
| if: >- | |
| !cancelled() && | |
| (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') && | |
| (inputs.prompt == '' || inputs['output-mode'] == 'working-dir') | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.FRO_BOT_PAT }} | |
| - name: 📦 Setup | |
| uses: ./.github/actions/setup | |
| - name: Sync wiki from data branch | |
| uses: ./.github/actions/sync-wiki | |
| - name: Capture wiki baseline | |
| id: wiki-baseline | |
| run: | | |
| git diff --no-ext-diff -- knowledge/index.md knowledge/log.md knowledge/wiki > "$RUNNER_TEMP/wiki-baseline.diff" | |
| echo "hash=$(shasum -a 256 "$RUNNER_TEMP/wiki-baseline.diff" | cut -d ' ' -f1)" >> "$GITHUB_OUTPUT" | |
| # Content-hash snapshot of every wiki-scoped path as it stands right after | |
| # sync-wiki, before the agent runs. wiki-handoff-build.ts diffs against this so | |
| # the handoff only carries what the agent actually changed — not every path | |
| # `data` differs from `main` on. See scripts/wiki-handoff-core.ts. | |
| - name: Capture wiki content baseline | |
| env: | |
| WIKI_HANDOFF_BASELINE_PATH: ${{ runner.temp }}/wiki-content-baseline-observe.json | |
| run: node scripts/wiki-handoff-baseline.ts | |
| - name: 🎭 Load Persona | |
| id: persona | |
| run: | | |
| delimiter="EOF_$(openssl rand -hex 8)" | |
| { | |
| echo "content<<$delimiter" | |
| cat persona/fro-bot-persona.md | |
| echo "$delimiter" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Query wiki context | |
| id: wiki-query | |
| env: | |
| WIKI_QUERY_EVENT_NAME: ${{ github.event_name }} | |
| WIKI_QUERY_OWNER: ${{ github.repository_owner }} | |
| WIKI_QUERY_REPO: ${{ github.event.repository.name }} | |
| WIKI_QUERY_TITLE: '' | |
| WIKI_QUERY_BODY: '' | |
| # Run-local handoff for optional wiki context expansion. | |
| # Contains only selected wiki paths — never logged, never echoed. | |
| WIKI_CONTEXT_HANDOFF_PATH: ${{ runner.temp }}/wiki-context-handoff-${{ github.run_id }}-${{ github.run_attempt }}-observe.json | |
| run: node scripts/wiki-query.ts | |
| # Overlay metadata/ from the data branch so this run can derive digest | |
| # counts and scan metadata/repos.yaml (category 6) from live data. | |
| # Mirrors the precedented guard in reconcile-repos.yaml: tolerate a | |
| # missing data branch (skip, don't fail). Runs BEFORE solutions-query | |
| # so the privacy scan reads the freshest repos.yaml. | |
| - name: ⤵ Overlay metadata from data branch | |
| run: | | |
| set -euo pipefail | |
| if git ls-remote --exit-code origin data >/dev/null 2>&1; then | |
| overlay_err=$( | |
| { git fetch --no-tags origin data && git checkout origin/data -- metadata/; } 2>&1 | |
| ) || { | |
| echo "::warning::metadata overlay fetch/checkout failed (continuing — counts will use stale/missing metadata): $(echo "$overlay_err" | head -c 200)" | |
| } | |
| else | |
| echo "data branch not yet established; skipping metadata overlay." | |
| fi | |
| - name: Query solutions context | |
| id: solutions-query | |
| env: | |
| SOLUTIONS_QUERY_EVENT_NAME: ${{ github.event_name }} | |
| SOLUTIONS_QUERY_OWNER: ${{ github.repository_owner }} | |
| SOLUTIONS_QUERY_REPO: ${{ github.event.repository.name }} | |
| SOLUTIONS_QUERY_TITLE: '' | |
| SOLUTIONS_QUERY_BODY: '' | |
| run: node scripts/solutions-query.ts | |
| - name: Run Fro Bot | |
| id: fro-bot-agent | |
| uses: fro-bot/agent@e6efc1f13ed05056cc9ba68d6f8fb5e71bed8ca6 # v0.117.0 | |
| env: | |
| OPENCODE_PROMPT_ARTIFACT: 'true' | |
| PERSONA: ${{ steps.persona.outputs.content }} | |
| WIKI_CONTEXT: ${{ steps.wiki-query.outputs.excerpt }} | |
| SOLUTIONS_CONTEXT: ${{ steps.solutions-query.outputs.excerpt }} | |
| WIKI_CONTEXT_HANDOFF_PATH: ${{ runner.temp }}/wiki-context-handoff-${{ github.run_id }}-${{ github.run_attempt }}-observe.json | |
| TASK_PROMPT: >- | |
| ${{ | |
| (github.event_name == 'workflow_dispatch' && github.event.inputs.prompt) | |
| || inputs.prompt | |
| || format('{0}{1}{2}{3}{4}{5}', env.OBSERVE_INTRO, env.SHARED_RULES, env.OBSERVE_CATEGORIES, env.HARD_BOUNDARIES, env.AGENT_NOTES, env.OBSERVE_OUTPUT) | |
| }} | |
| with: | |
| github-token: ${{ secrets.FRO_BOT_PAT }} | |
| auth-json: ${{ secrets.OPENCODE_AUTH_JSON }} | |
| model: ${{ vars.FRO_BOT_MODEL }} | |
| omo-providers: ${{ secrets.OMO_PROVIDERS }} | |
| opencode-config: ${{ secrets.OPENCODE_CONFIG }} | |
| output-mode: working-dir | |
| prompt: | | |
| <persona> | |
| ${{ env.PERSONA }} | |
| </persona> | |
| ${{ env.TASK_PROMPT }} | |
| <wiki_context> | |
| ${{ env.WIKI_CONTEXT }} | |
| </wiki_context> | |
| <solutions_context> | |
| ${{ env.SOLUTIONS_CONTEXT }} | |
| </solutions_context> | |
| <wiki_context_expansion> | |
| The wiki context above is baseline-injected and usually enough on its own — do not | |
| expand by default. If baseline context is insufficient, following linked pages would | |
| help, or you have a short grounded query, you may optionally run one of: | |
| `node scripts/wiki-context-expand.ts linked` | |
| `node scripts/wiki-context-expand.ts query "<short grounded query>"` | |
| Linked mode reads the baseline-selected pages from the path in | |
| `WIKI_CONTEXT_HANDOFF_PATH`; do not print or echo that path, its file contents, the | |
| query text, returned excerpts, or any token/env values into your output or logs. Use | |
| returned wiki paths only to ground a recommendation. | |
| </wiki_context_expansion> | |
| <knowledge_persistence> | |
| Use the wiki context when it materially improves the response. | |
| Consult the solutions context for prior learnings relevant to this work; | |
| when one applies, follow it and cite its `Path:` in your reasoning. Treat | |
| entries marked as candidate suggestions as hints to verify, not settled facts. | |
| If this interaction surfaces durable knowledge worth keeping, update only | |
| `knowledge/wiki/**`, `knowledge/index.md`, and `knowledge/log.md` following | |
| `knowledge/schema.md`. Keep changes additive, preserve contradictions with | |
| dates and sources, and leave the wiki untouched when the signal is weak. | |
| </knowledge_persistence> | |
| - name: Detect wiki insight changes | |
| id: wiki-changes | |
| if: always() && !cancelled() | |
| run: | | |
| git diff --no-ext-diff -- knowledge/index.md knowledge/log.md knowledge/wiki > "$RUNNER_TEMP/wiki-current.diff" | |
| current_hash=$(shasum -a 256 "$RUNNER_TEMP/wiki-current.diff" | cut -d ' ' -f1) | |
| if [ "$current_hash" = "${{ steps.wiki-baseline.outputs.hash }}" ]; then | |
| echo "changed=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "changed=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| # Build a data-only handoff artifact instead of writing `data` from this job | |
| # directly — the artifact carries only manifest.json + changed files, no ingest | |
| # metadata. WIKI_OPERATION/WIKI_TARGET/etc. are set from trusted github.* context | |
| # directly in the trusted job below. See scripts/wiki-handoff-core.ts. | |
| - name: Build wiki handoff artifact | |
| id: wiki-handoff-build | |
| if: >- | |
| success() && | |
| steps.wiki-changes.outputs.changed == 'true' && | |
| contains(fromJSON('["schedule","workflow_dispatch"]'), github.event_name) | |
| env: | |
| WIKI_HANDOFF_DIR: ${{ runner.temp }}/wiki-handoff-observe | |
| WIKI_HANDOFF_BASELINE_PATH: ${{ runner.temp }}/wiki-content-baseline-observe.json | |
| run: node scripts/wiki-handoff-build.ts | |
| # Baseline scoping (see the build step's env) can legitimately narrow a manifest | |
| # to empty even when the outer wiki-changes diff-hash flagged something — e.g. an | |
| # edit reverted before the run ended. An empty manifest must mean no artifact and | |
| # no downstream ingest, matching the pre-scoping no-op contract exactly. | |
| - name: Upload wiki handoff artifact | |
| if: steps.wiki-handoff-build.outcome == 'success' && steps.wiki-handoff-build.outputs.changed == 'true' | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: wiki-handoff-observe-${{ github.run_id }} | |
| path: ${{ runner.temp }}/wiki-handoff-observe | |
| retention-days: 1 | |
| # ── Trusted writer: derives and announces the daily digest ─────────────── | |
| # Fresh default-branch checkout, no agent step. The report URL and digest counts | |
| # are computed HERE, trusted-side, instead of read from fro-bot-observe's outputs | |
| # (which were produced by steps that run after the agent and would otherwise be | |
| # agent-influenceable). Strict shape validation stays as defense in depth. | |
| fro-bot-observe-announce: | |
| name: Fro Bot — Observe — Announce daily digest | |
| needs: fro-bot-observe | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| if: inputs.prompt == '' && vars.DAILY_DIGEST_ENABLED == 'true' | |
| steps: | |
| - name: Checkout trusted code (default branch) | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| persist-credentials: false | |
| - name: 📦 Setup | |
| uses: ./.github/actions/setup | |
| # Mirrors fro-bot-observe's own step: tolerate a missing data branch (skip, don't fail). | |
| - name: ⤵ Overlay metadata from data branch | |
| run: | | |
| set -euo pipefail | |
| if git ls-remote --exit-code origin data >/dev/null 2>&1; then | |
| overlay_err=$( | |
| { git fetch --no-tags origin data && git checkout origin/data -- metadata/; } 2>&1 | |
| ) || { | |
| echo "::warning::metadata overlay fetch/checkout failed (continuing — counts will use stale/missing metadata): $(echo "$overlay_err" | head -c 200)" | |
| } | |
| else | |
| echo "data branch not yet established; skipping metadata overlay." | |
| fi | |
| # This job has no agent step, so minting here is safe (see | |
| # scripts/agent-post-step-credential-guard.test.ts). Repo-scoped, read-only — | |
| # narrower than the FRO_BOT_PAT this step used to borrow from the observe agent. | |
| - name: 🔑 Mint App token for report discovery | |
| id: report-token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| app-id: ${{ secrets.APPLICATION_ID }} | |
| private-key: ${{ secrets.APPLICATION_PRIVATE_KEY }} | |
| repositories: ${{ github.event.repository.name }} | |
| permission-issues: read | |
| # Same query/title-match logic as fro-bot-observe's old inline step, run trusted-side. | |
| - name: 🔍 Discover daily report URL | |
| id: report-url | |
| env: | |
| GH_TOKEN: ${{ steps.report-token.outputs.token }} | |
| run: | | |
| set -euo pipefail | |
| TODAY_UTC="$(date -u +%Y-%m-%d)" | |
| EXPECTED_TITLE="Daily Fro Bot Report — ${TODAY_UTC} (UTC)" | |
| if ! MATCHES="$(gh issue list \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --state open \ | |
| --limit 50 \ | |
| --json number,title,url \ | |
| 2>/tmp/discover-err \ | |
| | jq --arg title "$EXPECTED_TITLE" \ | |
| '[.[] | select(.title == $title)]' \ | |
| 2>>/tmp/discover-err)"; then | |
| echo "report_url=" >> "$GITHUB_OUTPUT" | |
| echo "::warning::daily-digest: report URL discovery failed: $(head -c 200 /tmp/discover-err 2>/dev/null)" | |
| exit 0 | |
| fi | |
| COUNT="$(echo "$MATCHES" | jq 'length')" | |
| if [ "$COUNT" -ne 1 ]; then | |
| echo "report_url=" >> "$GITHUB_OUTPUT" | |
| echo "::notice::daily-digest: expected 1 report issue match, got ${COUNT}; skipping announce" | |
| else | |
| CANDIDATE_URL="$(echo "$MATCHES" | jq -r '.[0].url')" | |
| if echo "$CANDIDATE_URL" | grep -qE '^https://github\.com/fro-bot/\.github/issues/[0-9]+$'; then | |
| echo "report_url=${CANDIDATE_URL}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "report_url=" >> "$GITHUB_OUTPUT" | |
| echo "::notice::daily-digest: report URL failed shape validation; skipping announce" | |
| fi | |
| fi | |
| # Same derivation as fro-bot-observe's old inline step, run trusted-side. | |
| - name: 📊 Derive daily digest counts | |
| id: digest-counts | |
| run: | | |
| set -euo pipefail | |
| TODAY_UTC="$(date -u +%Y-%m-%d)" | |
| export TODAY_UTC | |
| RESULT="$(node scripts/daily-digest-counts.ts metadata/repos.yaml "$TODAY_UTC")" | |
| REPOS_TRACKED="$(echo "$RESULT" | jq '.repos_tracked')" | |
| SURVEYS_TODAY="$(echo "$RESULT" | jq '.surveys_today')" | |
| COUNT_STATUS="$(echo "$RESULT" | jq -r '.count_status')" | |
| { | |
| echo "repos_tracked=${REPOS_TRACKED}" | |
| echo "surveys_today=${SURVEYS_TODAY}" | |
| echo "count_status=${COUNT_STATUS}" | |
| } >> "$GITHUB_OUTPUT" | |
| if [ "$COUNT_STATUS" = "error" ]; then | |
| echo "::warning::daily-digest-counts: count_status=error — metadata overlay may have failed; treating as quiet day" | |
| fi | |
| # Fail-closed: reject anything that doesn't match the expected shape. An empty | |
| # REPORT_URL means the discovery step already emitted its own ::notice:: (nothing | |
| # found, or discovery failed) — don't double-report it as an error here. | |
| - name: 🔒 Validate digest values | |
| id: validated | |
| env: | |
| REPORT_URL: ${{ steps.report-url.outputs.report_url }} | |
| REPOS_TRACKED: ${{ steps.digest-counts.outputs.repos_tracked }} | |
| SURVEYS_TODAY: ${{ steps.digest-counts.outputs.surveys_today }} | |
| COUNT_STATUS: ${{ steps.digest-counts.outputs.count_status }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$COUNT_STATUS" != "ok" ]; then | |
| echo "::notice::daily-digest: count_status=${COUNT_STATUS}; skipping announce" | |
| echo "valid=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ -z "$REPORT_URL" ]; then | |
| echo "valid=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if ! printf '%s' "$REPORT_URL" | grep -qE '^https://github\.com/fro-bot/\.github/issues/[0-9]+$'; then | |
| echo "::error::daily-digest: report URL failed shape validation; skipping announce" | |
| echo "valid=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if ! printf '%s' "$REPOS_TRACKED" | grep -qE '^[0-9]+$'; then | |
| echo "::error::daily-digest: repos_tracked is not a non-negative integer; skipping announce" | |
| echo "valid=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if ! printf '%s' "$SURVEYS_TODAY" | grep -qE '^[0-9]+$'; then | |
| echo "::error::daily-digest: surveys_today is not a non-negative integer; skipping announce" | |
| echo "valid=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "valid=true" >> "$GITHUB_OUTPUT" | |
| # Best-effort Discord presence: announce the daily digest to the gateway. | |
| # DORMANT until vars.DAILY_DIGEST_ENABLED is set (default unset at merge). | |
| # Go-live: set DAILY_DIGEST_ENABLED after the gateway daily_digest variant is | |
| # deployed and verified in fro-bot/agent + marcusrbrown/infra. | |
| - name: 📣 Announce daily digest to gateway | |
| if: steps.validated.outputs.valid == 'true' | |
| env: | |
| EVENT_TYPE: daily_digest | |
| REPOS_TRACKED: ${{ steps.digest-counts.outputs.repos_tracked }} | |
| SURVEYS_TODAY: ${{ steps.digest-counts.outputs.surveys_today }} | |
| REPORT_URL: ${{ steps.report-url.outputs.report_url }} | |
| GATEWAY_PRESENCE_URL: ${{ vars.GATEWAY_PRESENCE_URL }} | |
| GATEWAY_WEBHOOK_SECRET: ${{ secrets.GATEWAY_WEBHOOK_SECRET }} | |
| GATEWAY_ANNOUNCE_DISABLED: ${{ vars.GATEWAY_ANNOUNCE_DISABLED }} | |
| run: | | |
| EVENT_CONTEXT_JSON="$(jq -nc \ | |
| --argjson repos_tracked "$REPOS_TRACKED" \ | |
| --argjson surveys_today "$SURVEYS_TODAY" \ | |
| --arg report_url "$REPORT_URL" \ | |
| '{repos_tracked: $repos_tracked, surveys_today: $surveys_today, report_url: $report_url}')" | |
| export EVENT_CONTEXT_JSON | |
| node scripts/gateway-announce.ts | |
| # ── Trusted writer: applies the fro-bot-observe job's wiki handoff artifact to `data` ── | |
| fro-bot-observe-wiki-ingest: | |
| name: Fro Bot — Observe — Ingest wiki insight | |
| needs: fro-bot-observe | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| if: needs.fro-bot-observe.outputs.wiki-changed == 'true' | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout trusted code (default branch) | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| persist-credentials: false | |
| - name: 📦 Setup | |
| uses: ./.github/actions/setup | |
| # Recomputed here (not reused from the agent job) so the ingest metadata is entirely | |
| # trusted-job-sourced: github.* context plus this job's own clock. | |
| - name: Capture ingest timestamp | |
| id: ingest-ts | |
| run: echo "now=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" | |
| - name: Sync wiki from data branch | |
| uses: ./.github/actions/sync-wiki | |
| - name: Download wiki handoff artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: wiki-handoff-observe-${{ github.run_id }} | |
| path: ${{ runner.temp }}/wiki-handoff | |
| - name: Validate and apply wiki handoff | |
| env: | |
| WIKI_HANDOFF_DIR: ${{ runner.temp }}/wiki-handoff | |
| run: node scripts/wiki-handoff-apply.ts | |
| - name: 🔑 Mint App token for wiki ingest | |
| id: app-token | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| app-id: ${{ secrets.APPLICATION_ID }} | |
| private-key: ${{ secrets.APPLICATION_PRIVATE_KEY }} | |
| repositories: ${{ github.event.repository.name }} | |
| permission-contents: write | |
| - name: Ingest wiki insight changes | |
| env: | |
| GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} | |
| WIKI_OPERATION: event | |
| WIKI_TARGET: 'repo:${{ github.repository }}' | |
| WIKI_SUMMARY: Persisted durable knowledge from the ${{ github.event_name }} interaction on ${{ github.repository }}. | |
| WIKI_COMMIT_MESSAGE: 'feat(knowledge): ingest ${{ github.event_name }} context for ${{ github.repository }}' | |
| WIKI_SOURCES: '[{"url":"https://github.com/${{ github.repository }}","sha":"${{ github.sha }}","accessed":"${{ steps.ingest-ts.outputs.now }}"}]' | |
| run: | | |
| warning_file="$RUNNER_TEMP/wiki-ingest-warnings" | |
| if node scripts/wiki-ingest.ts 2>"$warning_file"; then | |
| status=0 | |
| else | |
| status=$? | |
| fi | |
| cat "$warning_file" >&2 | |
| if grep -q '^wiki-ingest:warning:' "$warning_file"; then | |
| { | |
| echo '### Wiki ingest warnings' | |
| grep '^wiki-ingest:warning:' "$warning_file" | sed 's/^wiki-ingest:warning:/- /' | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| exit "$status" |