Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions actions/issue-health/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ responds to issue edits/reopens in v1.
| `repo` | no | current repo | `owner/name` of the issue. |
| `model` | no | `github/openai/gpt-4.1` | Override the issue-health model. |
| `override-mode` | no | `merge` | `merge` or `replace` — how local `.agents/` overrides combine with defaults. |
| `local-config-dir` | no | `.agents` | Path in the target repo to `.agents`-compatible local overrides. Use workflow-specific directories like `.agents/issue-health` to keep multiple agents isolated. |
| `github-token` | no | `github.token` | Token for `gh` and GitHub Models. Needs `issues: write`; the default GitHub model also needs `models: read`. |
| `comment-mode` | no | `always` | `always`, `needs-improvement`, or `off`. |
| `label-mode` | no | `existing-only` | `existing-only` or `off`. `existing-only` filters suggestions against labels already present in the target repo. |
Expand All @@ -57,13 +58,26 @@ via `env:` on the `uses:` step or at the job level rather than as action inputs.
## Per-repo overrides (`.agents/`)

A consuming repo can tailor the issue-health analysis by adding an `.agents/`
directory at its root. The action passes that directory to the workflow as
`ISSUE_HEALTH_TARGET_DIR`, and `override-mode` controls how those files combine
with central defaults:
directory at its root. The action passes the selected local override directory
to the workflow via `ISSUE_HEALTH_TARGET_DIR` plus `ISSUE_HEALTH_LOCAL_CONFIG_DIR`,
and `override-mode` controls how those files combine with central defaults:

- `merge` (default): central defaults apply first, then local files refine them.
- `replace`: local files replace central files for the kinds the repo provides.

When multiple fleet agents run in the same repo, keep their overrides isolated by
putting them in workflow-specific directories and setting `local-config-dir`:

```yaml
- uses: flipt-io/agents/actions/issue-health@main
with:
issue-number: ${{ github.event.issue.number }}
local-config-dir: .agents/issue-health
```

The directory still uses the same shape (`prompts/`, `skills/`, `personas/`) as
`.agents/`.

## How it works

The composite action resolves this agents repo, installs dependencies, and runs:
Expand All @@ -77,6 +91,7 @@ It sets the `ISSUE_HEALTH_*` environment variables consumed by

- `ISSUE_HEALTH_AGENT_DIR`
- `ISSUE_HEALTH_TARGET_DIR`
- `ISSUE_HEALTH_LOCAL_CONFIG_DIR`
- `ISSUE_HEALTH_OVERRIDE_MODE`
- `ISSUE_HEALTH_MODEL`
- `ISSUE_HEALTH_COMMENT_MODE`
Expand Down
5 changes: 5 additions & 0 deletions actions/issue-health/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ inputs:
description: 'How a consuming repo .agents/ directory combines with the central defaults: "merge" (defaults + local) or "replace" (local only).'
required: false
default: 'merge'
local-config-dir:
description: 'Path in the target repository to .agents-compatible local overrides. Defaults to .agents.'
required: false
default: '.agents'
github-token:
description: 'Token used by gh to read/comment on the issue, apply existing labels, and authenticate GitHub Models. Needs issues: write and models: read for the default github/* model.'
required: false
Expand Down Expand Up @@ -83,6 +87,7 @@ runs:
# <their repo>/.agents. The workflow layers them per override-mode.
ISSUE_HEALTH_AGENT_DIR: ${{ steps.agent.outputs.dir }}
ISSUE_HEALTH_TARGET_DIR: ${{ github.workspace }}
ISSUE_HEALTH_LOCAL_CONFIG_DIR: ${{ inputs.local-config-dir }}
ISSUE_HEALTH_OVERRIDE_MODE: ${{ inputs.override-mode }}
ISSUE_HEALTH_MODEL: ${{ inputs.model }}
ISSUE_HEALTH_COMMENT_MODE: ${{ inputs.comment-mode }}
Expand Down
31 changes: 22 additions & 9 deletions actions/pr-review/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Anthropic.
| `model` | no | `github/openai/gpt-4.1` | Override the review model. |
| `repo` | no | current repo | `owner/name` of the PR. |
| `override-mode` | no | `merge` | `merge` or `replace` — how local overrides combine with defaults. |
| `local-config-dir` | no | `.agents` | Path in the target repo to `.agents`-compatible local overrides. Use workflow-specific directories like `.agents/pr-review` to keep multiple agents isolated. |
| `github-token` | no | `github.token` | Token for `gh` and GitHub Models (needs `pull-requests: write`, and `models: read` for `github/*`). |

Provider credentials (`ANTHROPIC_API_KEY`, `CLOUDFLARE_API_KEY`,
Expand All @@ -52,10 +53,9 @@ See [Models](#models) for the full list per provider.

## Per-repo overrides (`.agents/`)

A consuming repo can tailor reviews by adding an `.agents/` directory at its root,
so the same directory can hold overrides for every fleet agent that reviews the
repo, not just this one. Anything present is layered on top of (or replaces) the
central defaults according to `override-mode`:
A consuming repo can tailor reviews by adding an `.agents/` directory at its root.
Anything present is layered on top of (or replaces) the central defaults according
to `override-mode`:

```
.agents/
Expand All @@ -77,18 +77,31 @@ central defaults according to `override-mode`:

Subdirectories the reviewer doesn't recognize (e.g. a repo's `.agents/commands/`)
are simply ignored, and `README.md` files in `prompts/` and `personas/` are
skipped. If a repo ships no `.agents/` directory, it just gets the central
defaults.
skipped. If a repo ships no `.agents/` directory, it just gets the central defaults.

When multiple fleet agents run in the same repo, keep their overrides isolated by
putting them in workflow-specific directories and setting `local-config-dir`:

```yaml
- uses: <owner>/agents/actions/pr-review@main
with:
pr-number: ${{ github.event.pull_request.number }}
local-config-dir: .agents/pr-review
```

The directory still uses the same shape (`prompts/`, `skills/`, `personas/`) as
`.agents/`.

## How it works

The action runs in the consuming repo's CI. `github.action_path` is the
checked-out agents repo, so the agent's code, skills, and prompts come along for
free at the pinned ref. The action installs deps, then runs
`flue run pr-review`, passing the PR coordinates plus the paths to the central
config (`REVIEW_AGENT_DIR`) and the consumer's checkout (`REVIEW_TARGET_DIR`).
The `code-review` skill resolves the layered guidance, fetches the diff with
`gh`, reviews, and posts the result.
config (`REVIEW_AGENT_DIR`), the consumer's checkout (`REVIEW_TARGET_DIR`), and
the selected local override directory (`REVIEW_LOCAL_CONFIG_DIR`, defaulting to
`.agents`). The `code-review` skill resolves the layered guidance, fetches the
diff with `gh`, reviews, and posts the result.

## Models

Expand Down
5 changes: 5 additions & 0 deletions actions/pr-review/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ inputs:
description: 'How a consuming repo .agents/ directory combines with the central defaults: "merge" (defaults + local) or "replace" (local only).'
required: false
default: 'merge'
local-config-dir:
description: 'Path in the target repository to .agents-compatible local overrides. Defaults to .agents.'
required: false
default: '.agents'
github-token:
description: 'Token used by gh to read the diff and post the review. Needs pull-requests: write.'
required: false
Expand Down Expand Up @@ -63,6 +67,7 @@ runs:
# <their repo>/.agents. The workflow layers them per override-mode.
REVIEW_AGENT_DIR: ${{ steps.agent.outputs.dir }}
REVIEW_TARGET_DIR: ${{ github.workspace }}
REVIEW_LOCAL_CONFIG_DIR: ${{ inputs.local-config-dir }}
REVIEW_OVERRIDE_MODE: ${{ inputs.override-mode }}
REVIEW_MODEL: ${{ inputs.model }}
run: |
Expand Down
1 change: 1 addition & 0 deletions examples/consumer-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
# Optional:
# model: github/openai/gpt-4o # another GitHub model
# override-mode: merge # or: replace
# local-config-dir: .agents/pr-review # isolate this agent's prompts from other agents

# --- Or review with Anthropic instead of GitHub Models -----------------
# Drop `models: read` above, add ANTHROPIC_API_KEY as a repo secret, and
Expand Down
1 change: 1 addition & 0 deletions examples/issue-health-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ jobs:
# label-mode: existing-only # existing-only | off; never creates labels
# model: github/openai/gpt-4o # another GitHub model
# override-mode: merge # or: replace
# local-config-dir: .agents/issue-health # isolate this agent's prompts from other agents

# --- Or use Anthropic instead of GitHub Models ------------------------
# Drop `models: read` above, add ANTHROPIC_API_KEY as a repo secret, and
Expand Down
2 changes: 2 additions & 0 deletions test/issue-health-action.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ test('issue-health action declares the expected inputs and config env vars', ()
'repo',
'model',
'override-mode',
'local-config-dir',
'github-token',
'comment-mode',
'label-mode',
Expand All @@ -22,6 +23,7 @@ test('issue-health action declares the expected inputs and config env vars', ()
for (const envName of [
'ISSUE_HEALTH_AGENT_DIR',
'ISSUE_HEALTH_TARGET_DIR',
'ISSUE_HEALTH_LOCAL_CONFIG_DIR',
'ISSUE_HEALTH_OVERRIDE_MODE',
'ISSUE_HEALTH_MODEL',
'ISSUE_HEALTH_COMMENT_MODE',
Expand Down
28 changes: 28 additions & 0 deletions test/local-config-dir.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';

const prAction = await readFile(new URL('../actions/pr-review/action.yml', import.meta.url), 'utf8');
const issueAction = await readFile(new URL('../actions/issue-health/action.yml', import.meta.url), 'utf8');
const prWorkflow = await readFile(new URL('../workflows/pr-review.ts', import.meta.url), 'utf8');
const issueWorkflow = await readFile(new URL('../workflows/issue-health.ts', import.meta.url), 'utf8');

test('PR review action supports a workflow-specific local config directory', () => {
assert.match(prAction, /^ local-config-dir:/m);
assert.match(prAction, /Defaults to \.agents/);
assert.match(prAction, /^ REVIEW_LOCAL_CONFIG_DIR: \$\{\{ inputs\.local-config-dir \}\}/m);
assert.match(prWorkflow, /REVIEW_LOCAL_CONFIG_DIR/);
assert.match(prWorkflow, /from 'node:path'/);
assert.match(prWorkflow, /path\.join\(targetDir, configuredDir/);
assert.match(prWorkflow, /resolveLocalConfigDir\(env\.REVIEW_TARGET_DIR, env\.REVIEW_LOCAL_CONFIG_DIR\)/);
});

test('issue-health action supports a workflow-specific local config directory', () => {
assert.match(issueAction, /^ local-config-dir:/m);
assert.match(issueAction, /Defaults to \.agents/);
assert.match(issueAction, /^ ISSUE_HEALTH_LOCAL_CONFIG_DIR: \$\{\{ inputs\.local-config-dir \}\}/m);
assert.match(issueWorkflow, /ISSUE_HEALTH_LOCAL_CONFIG_DIR/);
assert.match(issueWorkflow, /from 'node:path'/);
assert.match(issueWorkflow, /path\.join\(targetDir, configuredDir/);
assert.match(issueWorkflow, /resolveLocalConfigDir\(env\.ISSUE_HEALTH_TARGET_DIR, env\.ISSUE_HEALTH_LOCAL_CONFIG_DIR\)/);
});
13 changes: 11 additions & 2 deletions workflows/issue-health.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createAgent, type FlueContext, type FlueSession, type WorkflowRouteHandler } from '@flue/runtime';
import { local } from '@flue/runtime/node';
import path from 'node:path';
import * as v from 'valibot';
import {
ISSUE_HEALTH_ISSUE_TYPES,
Expand Down Expand Up @@ -75,6 +76,14 @@ type Config = {
labelMode: IssueHealthLabelMode;
};

function resolveLocalConfigDir(targetDir: string | undefined, localConfigDir: string | undefined): string {
if (!targetDir) return '';

const configuredDir = (localConfigDir?.trim() || '.agents').replace(/^\.\//, '').replace(/^[/\\]+/, '');

return path.join(targetDir, configuredDir);
}

function resolveConfig(env: Record<string, string | undefined>): Config {
const issueHealthConfig = resolveIssueHealthConfig(env);

Expand All @@ -83,8 +92,8 @@ function resolveConfig(env: Record<string, string | undefined>): Config {
agentDir: env.ISSUE_HEALTH_AGENT_DIR || '.',
// Checked-out target repo, used as context when actions provide it.
targetDir: env.ISSUE_HEALTH_TARGET_DIR || '',
// The target repo's optional `.agents/` overrides; '' when absent.
localConfigDir: env.ISSUE_HEALTH_TARGET_DIR ? `${env.ISSUE_HEALTH_TARGET_DIR}/.agents` : '',
// The target repo's optional local overrides; defaults to `.agents`.
localConfigDir: resolveLocalConfigDir(env.ISSUE_HEALTH_TARGET_DIR, env.ISSUE_HEALTH_LOCAL_CONFIG_DIR),
overrideMode: env.ISSUE_HEALTH_OVERRIDE_MODE === 'replace' ? 'replace' : 'merge',
model: env.ISSUE_HEALTH_MODEL || undefined,
...issueHealthConfig,
Expand Down
13 changes: 11 additions & 2 deletions workflows/pr-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
type WorkflowRouteHandler,
} from '@flue/runtime';
import { local } from '@flue/runtime/node';
import path from 'node:path';
import * as v from 'valibot';
import { postReview, renderReview } from '../lib/review-comments.ts';
import {
Expand Down Expand Up @@ -73,6 +74,14 @@ const PayloadSchema = v.object({
repo: v.optional(v.string()),
});

function resolveLocalConfigDir(targetDir: string | undefined, localConfigDir: string | undefined): string {
if (!targetDir) return '';

const configuredDir = (localConfigDir?.trim() || '.agents').replace(/^\.\//, '').replace(/^[/\\]+/, '');

return path.join(targetDir, configuredDir);
}

// Resolve where config lives, from env set by the GitHub Action. All have
// sensible fallbacks so a bare `flue run pr-review` still works locally.
function resolveConfig(env: Record<string, string | undefined>) {
Expand All @@ -81,8 +90,8 @@ function resolveConfig(env: Record<string, string | undefined>) {
agentDir: env.REVIEW_AGENT_DIR || '.',
// The checked-out repo under review (its source + its own AGENTS.md/README).
targetDir: env.REVIEW_TARGET_DIR || '',
// The under-review repo's optional `.agents/` overrides; '' when absent.
localConfigDir: env.REVIEW_TARGET_DIR ? `${env.REVIEW_TARGET_DIR}/.agents` : '',
// The under-review repo's optional local overrides; defaults to `.agents`.
localConfigDir: resolveLocalConfigDir(env.REVIEW_TARGET_DIR, env.REVIEW_LOCAL_CONFIG_DIR),
// How local overrides combine with central defaults.
overrideMode: env.REVIEW_OVERRIDE_MODE === 'replace' ? 'replace' : 'merge',
// Optional per-run model override (empty string -> use agent default).
Expand Down
Loading