Skip to content

Fro Bot

Fro Bot #6144

Workflow file for this run

---
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"