Nycto is a GitHub Action that triages new issues using Anthropic. When an issue is opened, Nycto:
- Searches for similar issues and existing workarounds
- Explores the source code to find where the problem lives
- Posts a structured comment with a solution, code investigation, and next steps
- Escalates complex design issues by applying a configurable label
Activity is traced to Opik for observability. Viewers can rate each response with a π/π reaction, which is synced back to Opik as human feedback β see Response feedback.
In your repository settings, add:
Secrets (Settings β Secrets and variables β Actions β Secrets):
| Secret | Description |
|---|---|
ANTHROPIC_API_KEY |
Anthropic API key |
OPIK_API_KEY |
Opik API key |
The
github.tokenbuilt-in is used for GitHub access β no personal access token or GitHub App required. Comments will appear asgithub-actions[bot].
Variables (Settings β Secrets and variables β Actions β Variables):
| Variable | Description |
|---|---|
NYCTO_GITHUB_REPO_OWNER |
Repository owner (e.g. comet-ml) |
NYCTO_GITHUB_REPO_NAME |
Repository name (e.g. opik) |
NYCTO_ESCALATION_TAG |
Label name for escalated issues (e.g. Escalated request) |
OPIK_WORKSPACE |
Opik workspace name |
Create .github/workflows/nycto.yml in your target repository:
name: Nycto Issue Triage
on:
issues:
types: [opened]
workflow_dispatch:
inputs:
issue_number:
description: Issue number to triage
required: true
type: number
# One Nycto run per issue at a time
concurrency:
group: nycto-issue-${{ github.event.issue.number || github.event.inputs.issue_number }}
cancel-in-progress: false
jobs:
triage:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
issues: write
contents: read
steps:
- name: Run Nycto
uses: comet-ml/nycto-repo-agent@main
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ github.token }}
env:
NYCTO_ESCALATION_TAG: ${{ vars.NYCTO_ESCALATION_TAG }}
NYCTO_GITHUB_REPO_OWNER: ${{ vars.NYCTO_GITHUB_REPO_OWNER }}
NYCTO_GITHUB_REPO_NAME: ${{ vars.NYCTO_GITHUB_REPO_NAME }}
OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }}
OPIK_WORKSPACE: ${{ vars.OPIK_WORKSPACE }}
ISSUE_NUMBER: ${{ github.event.issue.number || github.event.inputs.issue_number }}The GitHub App must have these permissions:
- Issues: Read & Write (to read issues and post comments)
- Contents: Read (to read source files)
| Env var | Required | Description |
|---|---|---|
ANTHROPIC_API_KEY |
yes | Anthropic API key |
GITHUB_TOKEN |
yes | GitHub token β pass ${{ github.token }} via the action input |
NYCTO_GITHUB_REPO_OWNER |
yes | Repo owner login |
NYCTO_GITHUB_REPO_NAME |
yes | Repo name |
NYCTO_ESCALATION_TAG |
no | Label for escalated issues (default: Escalated request) |
OPIK_API_KEY |
yes | Opik API key. Opik is required β Nycto sources its system prompt from Opik and traces every run there. |
OPIK_WORKSPACE |
yes | Opik workspace name |
NYCTO_FEEDBACK_SINCE_DAYS |
no | Feedback sync only: how many days back to scan issues for π/π reactions (default: 7) |
ISSUE_NUMBER |
no | Override issue number (auto-detected from event payload) |
NYCTO_MODEL |
no | Anthropic model ID (default: claude-sonnet-4-6) |
NYCTO_MAX_TOKENS |
no | Max response tokens (default: 8096) |
NYCTO_SYSTEM_PROMPT |
no | Override the system prompt inline. Supports $repo_owner, $repo_name, $escalation_tag placeholders. |
NYCTO_PROMPT_FILE |
no | Path to a file containing the system prompt (same placeholders supported). Takes effect only when NYCTO_SYSTEM_PROMPT is not set. |
NYCTO_OPIK_PROMPT_NAME |
no | Name of the Opik-managed prompt Nycto uses as its system prompt (default: nycto-system-prompt). If no prompt by this name exists in the project, Nycto auto-creates it from the built-in base prompt on first run. The Opik body is then used verbatim β no variable substitution β so edit it in the Opik UI to change behavior. For the GitHub Action, set via the opik_prompt_name action input rather than env: β see the Opik example below. |
NYCTO_OPIK_PROMPT_VERSION |
no | Pin a specific Opik prompt version (e.g. v3). Defaults to the latest version. For the GitHub Action, set via the opik_prompt_version action input. |
Nycto always sources its system prompt from Opik (Opik is required). On the first run for a project, if no prompt named NYCTO_OPIK_PROMPT_NAME (default nycto-system-prompt) exists in the project, Nycto creates it from a local base prompt and then uses the Opik copy verbatim on every run.
The base prompt β used only to seed Opik that first time β is resolved from NYCTO_SYSTEM_PROMPT (inline) > NYCTO_PROMPT_FILE (path to a file) > the built-in default. These three placeholders are substituted before the text is stored in Opik, so the stored prompt is fully resolved (no $-variables remain):
| Placeholder | Value |
|---|---|
$repo_owner |
Repository owner login |
$repo_name |
Repository name |
$escalation_tag |
Value of NYCTO_ESCALATION_TAG |
Once the Opik prompt exists, it is the source of truth β changing
NYCTO_SYSTEM_PROMPT/NYCTO_PROMPT_FILEno longer affects an already-seeded prompt. To change Nycto's behavior after bootstrap, edit the prompt in the Opik UI (see Using an Opik-managed prompt).
Example: prompt file in the workflow
Create .github/nycto-prompt.txt in your target repository:
You are Nycto π¦, a triage agent for $repo_owner/$repo_name.
For each new issue:
1. Search for duplicates using search_issues.
2. Identify the relevant source files with list_directory and get_file_contents.
3. Reply with a short summary, the affected file(s), and a suggested fix.
If the fix requires a breaking API change, call apply_label("$escalation_tag") before replying.
Keep responses concise and technical. Do not use filler phrases.
Then pass it to Nycto in your workflow:
- name: Run Nycto
uses: comet-ml/nycto-repo-agent@main
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ github.token }}
env:
NYCTO_ESCALATION_TAG: ${{ vars.NYCTO_ESCALATION_TAG }}
NYCTO_GITHUB_REPO_OWNER: ${{ vars.NYCTO_GITHUB_REPO_OWNER }}
NYCTO_GITHUB_REPO_NAME: ${{ vars.NYCTO_GITHUB_REPO_NAME }}
OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }}
OPIK_WORKSPACE: ${{ vars.OPIK_WORKSPACE }}
NYCTO_PROMPT_FILE: ${{ github.workspace }}/.github/nycto-prompt.txtExample: inline prompt via NYCTO_SYSTEM_PROMPT
For shorter prompts you can set the value directly as a GitHub Actions variable (Settings β Secrets and variables β Actions β Variables):
- name: Run Nycto
uses: comet-ml/nycto-repo-agent@main
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ github.token }}
env:
NYCTO_ESCALATION_TAG: ${{ vars.NYCTO_ESCALATION_TAG }}
NYCTO_GITHUB_REPO_OWNER: ${{ vars.NYCTO_GITHUB_REPO_OWNER }}
NYCTO_GITHUB_REPO_NAME: ${{ vars.NYCTO_GITHUB_REPO_NAME }}
OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }}
OPIK_WORKSPACE: ${{ vars.OPIK_WORKSPACE }}
NYCTO_SYSTEM_PROMPT: ${{ vars.NYCTO_SYSTEM_PROMPT }}When both
NYCTO_SYSTEM_PROMPTandNYCTO_PROMPT_FILEare set,NYCTO_SYSTEM_PROMPTtakes precedence.
Nycto's system prompt lives in Opik as a versioned artifact you can evaluate with Opik's Test Suite and improve with the Opik prompt optimizer, without redeploying the action. This is the default and only path β Nycto bootstraps the prompt automatically (see Customizing the system prompt), so you don't have to create it by hand.
Bootstrap (automatic): the first run with no existing prompt creates nycto-system-prompt (version v1) from the base prompt. Nothing to do.
Pre-create it (optional): to control the body up front, create a prompt in the Opik UI before the first run. Nycto uses it verbatim, so write any repo-specific values (owner, repo name, escalation tag) directly into the text. Use the same name you pass as opik_prompt_name (default nycto-system-prompt).
Reference a specific name/version from the workflow:
- name: Run Nycto
uses: comet-ml/nycto-repo-agent@main
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ github.token }}
opik_prompt_name: nycto-system-prompt
# opik_prompt_version: v3 # optional; omit to use the latest version
env:
NYCTO_ESCALATION_TAG: ${{ vars.NYCTO_ESCALATION_TAG }}
NYCTO_GITHUB_REPO_OWNER: ${{ vars.NYCTO_GITHUB_REPO_OWNER }}
NYCTO_GITHUB_REPO_NAME: ${{ vars.NYCTO_GITHUB_REPO_NAME }}
OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }}
OPIK_WORKSPACE: ${{ vars.OPIK_WORKSPACE }}
ISSUE_NUMBER: ${{ github.event.issue.number || github.event.inputs.issue_number }}Seed precedence (first run only): NYCTO_SYSTEM_PROMPT > NYCTO_PROMPT_FILE > built-in default. Once the Opik prompt exists, Opik is the source of truth. If a fetch fails transiently (e.g. network error), Nycto logs a warning and falls back to the local base prompt for that run so triage still completes.
Iterating: edit the prompt in Opik to publish a new version. Without NYCTO_OPIK_PROMPT_VERSION set, the next Nycto run picks it up automatically; with a pinned version, the run continues to use that version until you bump the value.
Anyone viewing an issue can rate Nycto's triage comment by adding a π or π reaction to it on GitHub. Those reactions are recorded in Opik as a human feedback score named user_feedback on the comment's trace:
- 1.0 = all π, 0.0 = all π, otherwise the ratio
π / (π + π)(e.g. 3 π and 1 π β0.75). Reactions other than π/π are ignored. - The score carries a
reasonthat attributes the votes by GitHub login, e.g.π 2 (alice, bob) / π 1 (carol) from GitHub, so you can see who reacted in the Opik UI alongside the trace.
How it works. GitHub fires no event when a reaction is added, so a scheduled workflow polls recent issues every 30 minutes, reads the reaction counts on Nycto's comments, and upserts the score. Each Nycto comment carries a hidden marker (<!-- nycto-feedback trace_id=β¦ -->) that maps it back to its Opik trace. The sync is idempotent β re-running simply recomputes the score from current reactions β so feedback lands in Opik within one cron interval and self-corrects as votes change.
Enabling it. The feedback sync ships as a second action published from this repo, comet-ml/nycto-repo-agent/actions/feedback, alongside the triage action. Add a scheduled workflow to the repo that runs Nycto β it reuses the same OPIK_API_KEY secret and OPIK_WORKSPACE value as the triage action:
name: Nycto Feedback Sync
on:
schedule:
- cron: '*/30 * * * *' # every 30 minutes
workflow_dispatch:
inputs:
since_days:
description: How many days back to scan issues for reactions
required: false
default: '7'
type: string
concurrency:
group: nycto-feedback-sync
cancel-in-progress: false
jobs:
sync-feedback:
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
issues: read
contents: read
steps:
- name: Sync reactions to Opik
uses: comet-ml/nycto-repo-agent/actions/feedback@main
with:
github_token: ${{ github.token }}
since_days: ${{ github.event.inputs.since_days || '7' }}
env:
OPIK_API_KEY: ${{ secrets.OPIK_API_KEY }}
OPIK_WORKSPACE: ${{ vars.OPIK_WORKSPACE }}Trigger it manually from the Actions tab for an immediate sync.
Scan window. GitHub does not bump an issue's
updated_atwhen a reaction is added, so the sync only re-checks issues with other activity withinNYCTO_FEEDBACK_SINCE_DAYS(default 7). Reactions on otherwise-quiet older issues may be missed β run the workflow manually with a largersince_daysto backfill. Because the upsert is idempotent, re-syncing is always safe.
Use the manual trigger workflow in this repo's Actions tab (Test Nycto (Manual)) to run Nycto against a specific issue number before enabling the automatic trigger.
The code is an installable package under src/nycto/. Install it (with dev extras) in editable mode:
pip install -e ".[dev]"
# Copy and fill in the template
cp .env.example .env
# Run triage locally (console script registered by the install).
# Equivalent to `python -m nycto.triage`.
nycto-triageRun the unit tests and linters with pytest, ruff check ., and mypy src/nycto.
.env.example:
ANTHROPIC_API_KEY=
GITHUB_TOKEN=github_pat_...
NYCTO_ESCALATION_TAG=Escalated request
NYCTO_GITHUB_REPO_OWNER=owner
NYCTO_GITHUB_REPO_NAME=name
ISSUE_NUMBER=123
OPIK_WORKSPACE=comet-all
OPIK_API_KEY=
# Optional: override the system prompt (supports $repo_owner, $repo_name, $escalation_tag)
# NYCTO_SYSTEM_PROMPT=
# NYCTO_PROMPT_FILE=
# Optional: fetch the system prompt from Opik (body used verbatim β no variable substitution)
# NYCTO_OPIK_PROMPT_NAME=
# NYCTO_OPIK_PROMPT_VERSION=