Compare commits

...

5 Commits

Author SHA1 Message Date
Vincent Koc
d41346b090
chore(queue): park failed clownfish drip jobs 2026-04-28 20:04:55 -07:00
Vincent Koc
f235dafeb1
chore(queue): park failed clownfish wave jobs 2026-04-28 17:53:31 -07:00
Vincent Koc
cf213a8d7d
chore(queue): park timed out clownfish job 2026-04-28 17:10:59 -07:00
Vincent Koc
c9abff105d
fix(auth): narrow clownfish app token permissions 2026-04-28 13:05:44 -07:00
Vincent Koc
e94a689db5
chore(auth): use clownfish app tokens 2026-04-28 13:02:17 -07:00
37 changed files with 96 additions and 31 deletions

View File

@ -73,6 +73,8 @@ jobs:
CLOWNFISH_MODEL: ${{ inputs.model || vars.CLOWNFISH_MODEL || 'gpt-5.5' }}
CODEX_CLI_VERSION: ${{ vars.CLOWNFISH_CODEX_CLI_VERSION || '0.125.0' }}
OPENCLAW_LOCAL_CHECK: "0"
CLOWNFISH_APP_ID: ${{ vars.CLOWNFISH_APP_ID || secrets.CLOWNFISH_APP_ID }}
CLOWNFISH_APP_AUTH_ENABLED: ${{ secrets.CLOWNFISH_APP_PRIVATE_KEY != '' && (vars.CLOWNFISH_APP_ID != '' || secrets.CLOWNFISH_APP_ID != '') && '1' || '0' }}
steps:
- uses: actions/checkout@v5
@ -99,12 +101,27 @@ jobs:
restore-keys: |
${{ runner.os }}-node24-codex-
- name: Create GitHub App read token
id: read_app_token
if: ${{ env.CLOWNFISH_APP_AUTH_ENABLED == '1' }}
uses: actions/create-github-app-token@v3
with:
app-id: ${{ env.CLOWNFISH_APP_ID }}
private-key: ${{ secrets.CLOWNFISH_APP_PRIVATE_KEY }}
owner: openclaw
repositories: |
openclaw
clownfish
permission-contents: read
permission-issues: read
permission-pull-requests: read
- name: Verify GitHub read token
env:
GH_TOKEN: ${{ secrets.CLOWNFISH_READ_GH_TOKEN }}
GH_TOKEN: ${{ steps.read_app_token.outputs.token || secrets.CLOWNFISH_READ_GH_TOKEN }}
run: |
if [ -z "${GH_TOKEN:-}" ]; then
echo "CLOWNFISH_READ_GH_TOKEN is required"
echo "CLOWNFISH_APP_ID + CLOWNFISH_APP_PRIVATE_KEY or CLOWNFISH_READ_GH_TOKEN is required"
exit 1
fi
gh auth status
@ -133,7 +150,7 @@ jobs:
- name: Run worker
env:
GH_TOKEN: ${{ secrets.CLOWNFISH_READ_GH_TOKEN }}
GH_TOKEN: ${{ steps.read_app_token.outputs.token || secrets.CLOWNFISH_READ_GH_TOKEN }}
run: |
args=("${{ inputs.job }}" --mode "${{ inputs.mode }}")
if [ "${{ inputs.dry_run }}" = "true" ]; then
@ -193,6 +210,8 @@ jobs:
CLOWNFISH_GIT_USER_EMAIL: ${{ vars.CLOWNFISH_GIT_USER_EMAIL || 'projectclownfish@users.noreply.github.com' }}
CODEX_CLI_VERSION: ${{ vars.CLOWNFISH_CODEX_CLI_VERSION || '0.125.0' }}
OPENCLAW_LOCAL_CHECK: "0"
CLOWNFISH_APP_ID: ${{ vars.CLOWNFISH_APP_ID || secrets.CLOWNFISH_APP_ID }}
CLOWNFISH_APP_AUTH_ENABLED: ${{ secrets.CLOWNFISH_APP_PRIVATE_KEY != '' && (vars.CLOWNFISH_APP_ID != '' || secrets.CLOWNFISH_APP_ID != '') && '1' || '0' }}
steps:
- uses: actions/checkout@v5
@ -213,6 +232,31 @@ jobs:
${{ runner.os }}-node24-codex-${{ env.CODEX_CLI_VERSION }}-target-pnpm-
${{ runner.os }}-node24-codex-
- name: Create GitHub App write token
id: write_app_token
if: ${{ env.CLOWNFISH_APP_AUTH_ENABLED == '1' }}
uses: actions/create-github-app-token@v3
with:
app-id: ${{ env.CLOWNFISH_APP_ID }}
private-key: ${{ secrets.CLOWNFISH_APP_PRIVATE_KEY }}
owner: openclaw
repositories: |
openclaw
clownfish
permission-contents: write
permission-issues: write
permission-pull-requests: write
- name: Verify GitHub write token
env:
GH_TOKEN: ${{ steps.write_app_token.outputs.token || secrets.CLOWNFISH_GH_TOKEN }}
run: |
if [ -z "${GH_TOKEN:-}" ]; then
echo "CLOWNFISH_APP_ID + CLOWNFISH_APP_PRIVATE_KEY or CLOWNFISH_GH_TOKEN is required"
exit 1
fi
gh auth status
- name: Install Codex CLI
run: |
set -euo pipefail
@ -245,31 +289,31 @@ jobs:
if: ${{ env.CLOWNFISH_ALLOW_EXECUTE == '1' && env.CLOWNFISH_ALLOW_FIX_PR == '1' }}
timeout-minutes: 30
env:
GH_TOKEN: ${{ secrets.CLOWNFISH_GH_TOKEN }}
GH_TOKEN: ${{ steps.write_app_token.outputs.token || secrets.CLOWNFISH_GH_TOKEN }}
run: npm run execute-fix -- "${{ inputs.job }}" --latest
- name: Apply safe closure actions
if: ${{ env.CLOWNFISH_ALLOW_EXECUTE == '1' }}
env:
GH_TOKEN: ${{ secrets.CLOWNFISH_GH_TOKEN }}
GH_TOKEN: ${{ steps.write_app_token.outputs.token || secrets.CLOWNFISH_GH_TOKEN }}
run: npm run apply-result -- "${{ inputs.job }}" --latest
- name: Post-flight finalize fix PRs
if: ${{ env.CLOWNFISH_ALLOW_EXECUTE == '1' && env.CLOWNFISH_ALLOW_FIX_PR == '1' }}
env:
GH_TOKEN: ${{ secrets.CLOWNFISH_GH_TOKEN }}
GH_TOKEN: ${{ steps.write_app_token.outputs.token || secrets.CLOWNFISH_GH_TOKEN }}
run: npm run post-flight -- "${{ inputs.job }}" --latest
- name: Apply post-flight closeouts
if: ${{ env.CLOWNFISH_ALLOW_EXECUTE == '1' }}
env:
GH_TOKEN: ${{ secrets.CLOWNFISH_GH_TOKEN }}
GH_TOKEN: ${{ steps.write_app_token.outputs.token || secrets.CLOWNFISH_GH_TOKEN }}
run: npm run apply-result -- "${{ inputs.job }}" --latest
- name: Tag Clownfish targets
if: ${{ always() && env.CLOWNFISH_ALLOW_EXECUTE == '1' }}
env:
GH_TOKEN: ${{ secrets.CLOWNFISH_GH_TOKEN }}
GH_TOKEN: ${{ steps.write_app_token.outputs.token || secrets.CLOWNFISH_GH_TOKEN }}
run: npm run tag-clownfish -- .projectclownfish/runs --apply --live --open-branches false --report .projectclownfish/runs/clownfish-label-report.json
- name: Upload final worker artifacts

View File

@ -14,6 +14,8 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
CLOWNFISH_APP_ID: ${{ vars.CLOWNFISH_APP_ID || secrets.CLOWNFISH_APP_ID }}
CLOWNFISH_APP_AUTH_ENABLED: ${{ secrets.CLOWNFISH_APP_PRIVATE_KEY != '' && (vars.CLOWNFISH_APP_ID != '' || secrets.CLOWNFISH_APP_ID != '') && '1' || '0' }}
concurrency:
group: projectclownfish-publish-results
@ -32,6 +34,21 @@ jobs:
with:
node-version: "24"
- name: Create GitHub App read token
id: read_app_token
if: ${{ env.CLOWNFISH_APP_AUTH_ENABLED == '1' }}
uses: actions/create-github-app-token@v3
with:
app-id: ${{ env.CLOWNFISH_APP_ID }}
private-key: ${{ secrets.CLOWNFISH_APP_PRIVATE_KEY }}
owner: openclaw
repositories: |
openclaw
clownfish
permission-contents: read
permission-issues: read
permission-pull-requests: read
- name: Download worker artifacts
env:
GH_TOKEN: ${{ github.token }}
@ -50,7 +67,7 @@ jobs:
- name: Publish and commit result ledger
env:
GH_TOKEN: ${{ secrets.CLOWNFISH_READ_GH_TOKEN || github.token }}
GH_TOKEN: ${{ steps.read_app_token.outputs.token || secrets.CLOWNFISH_READ_GH_TOKEN || github.token }}
RUN_ID: ${{ github.event.workflow_run.id }}
RUN_URL: ${{ github.event.workflow_run.html_url }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}

View File

@ -224,7 +224,7 @@ Each cluster job:
8. Applies guarded close/comment and explicit merge actions through `scripts/apply-result.mjs`.
9. Publishes a sanitized result ledger back to this repo under `results/`, `jobs/openclaw/closed/`, `apply-report.json`, and this README dashboard.
Codex does not receive a GitHub token during classification. The runner preflights GitHub state before model execution, then Codex receives those artifacts and returns JSON only. When a reviewed fix artifact is executed, Codex gets a temporary target checkout without GitHub credentials; the deterministic executor owns commit, push, PR creation, and source-PR closeout using `CLOWNFISH_GH_TOKEN`. Commit author metadata defaults to `projectclownfish` and can be overridden with `CLOWNFISH_GIT_USER_NAME` and `CLOWNFISH_GIT_USER_EMAIL`; this is separate from the GitHub token used to push. The applicator re-fetches the target item, checks `updated_at`, blocks unsafe closeouts, writes idempotent close comments, closes supported duplicate/superseded/fixed-by-candidate actions, and can squash-merge explicitly allowed clean PR actions.
Codex does not receive a GitHub token during classification. The runner preflights GitHub state before model execution, then Codex receives those artifacts and returns JSON only. The preferred GitHub auth path is a short-lived installation token minted from `CLOWNFISH_APP_ID` and `CLOWNFISH_APP_PRIVATE_KEY`; legacy `CLOWNFISH_READ_GH_TOKEN` and `CLOWNFISH_GH_TOKEN` secrets remain fallbacks. The read token is narrowed to read-only issue, PR, content, checks, and status access. When a reviewed fix artifact is executed, Codex gets a temporary target checkout without GitHub credentials; the deterministic executor owns commit, push, PR creation, and source-PR closeout using a write-scoped GitHub App token or `CLOWNFISH_GH_TOKEN`. Commit author metadata defaults to `projectclownfish` and can be overridden with `CLOWNFISH_GIT_USER_NAME` and `CLOWNFISH_GIT_USER_EMAIL`; this is separate from the GitHub token used to push. The applicator re-fetches the target item, checks `updated_at`, blocks unsafe closeouts, writes idempotent close comments, closes supported duplicate/superseded/fixed-by-candidate actions, and can squash-merge explicitly allowed clean PR actions.
Merge is deliberately harder than closeout. A merge action must include `merge_preflight` proving security clearance, resolved human comments, resolved review-bot findings, a passed Codex `/review`, addressed review findings, and clean validation commands. The fix executor runs an agentic edit/review loop before it writes a fix PR: edit, validate, Codex `/review`, address findings, revalidate, and resolve PR review threads when permitted. The applicator also checks live unresolved GitHub review threads immediately before merge.
@ -234,6 +234,10 @@ Runs for the same job path and mode are queued instead of running concurrently.
Full worker prompts, Codex transcripts, and raw artifacts stay in GitHub Actions. The committed ledger keeps only the cluster summary, run URL, action counts, apply outcomes, closed targets, and needs-human entries.
## GitHub App Auth
Create a GitHub App installed on `openclaw/openclaw` and `openclaw/clownfish`. Give it `Contents: write`, `Issues: write`, and `Pull requests: write`; leave webhooks disabled. Store the App ID as repository variable `CLOWNFISH_APP_ID` and the downloaded private key PEM as repository secret `CLOWNFISH_APP_PRIVATE_KEY` in `openclaw/clownfish`. The workflows mint per-job tokens with the minimum permission level needed for that job, so classification stays read-only and execution gets write access only after the execution gate opens. Merge remains disabled unless `CLOWNFISH_ALLOW_MERGE=1`.
## Modes
- `plan`: produces recommendations only.
@ -242,7 +246,7 @@ Full worker prompts, Codex transcripts, and raw artifacts stay in GitHub Actions
- `route_security`: quarantines true security-sensitive refs without poisoning unrelated cluster work.
- `needs_human`: only product-direction, trust-boundary, canonical-choice, merge-path, or contributor-credit decisions that remain unclear after the hydrated artifact and single-item review/check/decide pass.
- Automated reviewer feedback must be cleared during autonomous PR work. Greptile, Codex, Asile, CodeRabbit, Copilot, and similar bot comments must be addressed, proven non-actionable, or escalated before any merge or post-merge closeout recommendation.
- Merge preflight: no PR can merge until `CLOWNFISH_ALLOW_MERGE=1`, security issues are cleared, comments are resolved, Codex `/review` has passed, findings are addressed, and changed-surface validation is clean. With the merge gate closed, ProjectClownfish labels merge-ready targets for human review instead of merging.
- Merge preflight: no PR can merge until `CLOWNFISH_ALLOW_MERGE=1`, security issues are cleared, comments are resolved, Codex `/review` has passed, findings are addressed, and changed-surface validation is clean. With the merge gate closed, ProjectClownfish applies the single `clownfish` label and leaves the final merge to a maintainer.
- Repair ladder: make the useful contributor PR mergeable when its branch is maintainer-editable; otherwise replace draft, stale, unmergeable, uneditable, or unsafe branches with a narrow credited fix PR. When fix PR mode is enabled, "wait or replace" is already answered: replace, preserve credit, then supersede only the source PR that could not be safely updated.
## Local Run
@ -349,7 +353,7 @@ The workflow needs:
- a read-only GitHub token for worker inspection
- a separate write-scoped GitHub token for the deterministic applicator
- execution gates that default on for execute/autonomous jobs: set `CLOWNFISH_ALLOW_EXECUTE=0` or `CLOWNFISH_ALLOW_FIX_PR=0` only when intentionally pausing live work
- merge is separately gated by `CLOWNFISH_ALLOW_MERGE`; it defaults to `0`, and merge-ready PRs are labeled `clownfish:human-review` and `clownfish:merge-ready` for a maintainer to merge manually
- merge is separately gated by `CLOWNFISH_ALLOW_MERGE`; it defaults to `0`, and merge-ready PRs keep only the orange `clownfish` label for a maintainer to merge manually
- optional `CLOWNFISH_CODEX_CLI_VERSION` variable to pin and refresh the cached Codex CLI
- optional `CLOWNFISH_MODEL` override for dispatch scripts; default Codex model is `gpt-5.5`
- optional `CLOWNFISH_MAX_LIVE_WORKERS` variable for dispatch/requeue/self-heal worker fan-out; default is `50`

View File

@ -18,8 +18,9 @@ const MERGE_ACTIONS = new Set(["merge_candidate", "merge_canonical"]);
const CLOSE_CLASSIFICATIONS = new Set(["duplicate", "superseded", "fixed_by_candidate", "low_signal"]);
const PASSING_CHECK_CONCLUSIONS = new Set(["SUCCESS", "SKIPPED", "NEUTRAL"]);
const CLEAN_MERGE_STATES = new Set(["CLEAN"]);
const HUMAN_REVIEW_LABEL = "clownfish:human-review";
const MERGE_READY_LABEL = "clownfish:merge-ready";
const CLOWNFISH_LABEL = "clownfish";
const CLOWNFISH_LABEL_COLOR = "F97316";
const CLOWNFISH_LABEL_DESCRIPTION = "Tracked by Clownfish automation";
const args = parseArgs(process.argv.slice(2));
const jobPath = args._[0];
@ -406,11 +407,11 @@ function applyMergeAction({ job, result, action, dryRun, allowMissingUpdatedAt,
}
if (process.env.CLOWNFISH_ALLOW_MERGE !== "1") {
if (!dryRun) labelForHumanMergeReview(result.repo, target);
if (!dryRun) labelForClownfishReview(result.repo, target);
return {
...base,
status: "blocked",
reason: "merge requires CLOWNFISH_ALLOW_MERGE=1; labeled for human review",
reason: "merge requires CLOWNFISH_ALLOW_MERGE=1; labeled clownfish",
live_state: live.state,
live_updated_at: live.updated_at,
merge_method: "squash",
@ -494,11 +495,9 @@ function validateMergePolicy({ job, action }) {
return "";
}
function labelForHumanMergeReview(repo, target) {
ensureLabel(repo, HUMAN_REVIEW_LABEL, "B60205", "Needs maintainer review before ProjectClownfish can finish");
ensureLabel(repo, MERGE_READY_LABEL, "0E8A16", "ProjectClownfish found a merge-ready candidate; human owns the final merge");
ghBestEffort(["issue", "edit", String(target), "--repo", repo, "--add-label", HUMAN_REVIEW_LABEL]);
ghBestEffort(["issue", "edit", String(target), "--repo", repo, "--add-label", MERGE_READY_LABEL]);
function labelForClownfishReview(repo, target) {
ensureLabel(repo, CLOWNFISH_LABEL, CLOWNFISH_LABEL_COLOR, CLOWNFISH_LABEL_DESCRIPTION);
ghBestEffort(["issue", "edit", String(target), "--repo", repo, "--add-label", CLOWNFISH_LABEL]);
}
function ensureLabel(repo, name, color, description) {

View File

@ -11,8 +11,9 @@ const FIX_PR_ACTIONS = new Set(["open_fix_pr", "repair_contributor_branch"]);
const FIX_PR_READY_STATUSES = new Set(["opened", "pushed"]);
const POST_MERGE_CLOSE_ACTIONS = new Set(["close_duplicate", "close_superseded", "close_fixed_by_candidate", "post_merge_close"]);
const DEFAULT_IGNORED_CHECKS = ["auto-response", "Labeler", "Stale"];
const HUMAN_REVIEW_LABEL = "clownfish:human-review";
const MERGE_READY_LABEL = "clownfish:merge-ready";
const CLOWNFISH_LABEL = "clownfish";
const CLOWNFISH_LABEL_COLOR = "F97316";
const CLOWNFISH_LABEL_DESCRIPTION = "Tracked by Clownfish automation";
const POST_FLIGHT_WAIT_MS = numberEnv("CLOWNFISH_POST_FLIGHT_WAIT_MS", 10 * 60 * 1000);
const POST_FLIGHT_POLL_MS = numberEnv("CLOWNFISH_POST_FLIGHT_POLL_MS", 15 * 1000);
@ -161,11 +162,11 @@ function finalizeFixPr(action) {
}
if (process.env.CLOWNFISH_ALLOW_MERGE !== "1") {
labelForHumanMergeReview(result.repo, parsed.number);
labelForClownfishReview(result.repo, parsed.number);
return {
...prBase,
status: "blocked",
reason: "merge requires CLOWNFISH_ALLOW_MERGE=1; labeled for human review",
reason: "merge requires CLOWNFISH_ALLOW_MERGE=1; labeled clownfish",
merge_method: "squash",
waited_ms: waitedMs,
};
@ -288,11 +289,9 @@ function validateMergePolicy() {
return "";
}
function labelForHumanMergeReview(repo, number) {
ensureLabel(repo, HUMAN_REVIEW_LABEL, "B60205", "Needs maintainer review before ProjectClownfish can finish");
ensureLabel(repo, MERGE_READY_LABEL, "0E8A16", "ProjectClownfish found a merge-ready candidate; human owns the final merge");
ghBestEffort(["issue", "edit", String(number), "--repo", repo, "--add-label", HUMAN_REVIEW_LABEL]);
ghBestEffort(["issue", "edit", String(number), "--repo", repo, "--add-label", MERGE_READY_LABEL]);
function labelForClownfishReview(repo, number) {
ensureLabel(repo, CLOWNFISH_LABEL, CLOWNFISH_LABEL_COLOR, CLOWNFISH_LABEL_DESCRIPTION);
ghBestEffort(["issue", "edit", String(number), "--repo", repo, "--add-label", CLOWNFISH_LABEL]);
}
function ensureLabel(repo, name, color, description) {

View File

@ -5,6 +5,8 @@ import { execFileSync } from "node:child_process";
import { currentProjectRepo, parseArgs, parseSimpleYaml, repoRoot } from "./lib.mjs";
const DEFAULT_LABEL = "clownfish";
const DEFAULT_LABEL_COLOR = "F97316";
const DEFAULT_LABEL_DESCRIPTION = "Tracked by Clownfish automation";
const FIX_PR_STATUSES = new Set(["opened", "pushed", "executed", "blocked", "planned"]);
const APPLY_STATUSES = new Set(["executed"]);
const CLOSE_ACTIONS = new Set([
@ -327,7 +329,7 @@ function createGithubLabel() {
const repo = process.env.CLOWNFISH_TARGET_REPO ?? "openclaw/openclaw";
execFileSync(
"gh",
["label", "create", labelName, "--repo", repo, "--color", "0E8A16", "--description", "Tracked by Clownfish automation"],
["label", "create", labelName, "--repo", repo, "--color", DEFAULT_LABEL_COLOR, "--description", DEFAULT_LABEL_DESCRIPTION],
{ cwd: repoRoot(), encoding: "utf8", env: process.env, stdio: ["ignore", "pipe", "pipe"] },
);
}