Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 16 additions & 0 deletions .github/agents/registry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
version: 1

default_agent: codex

agents:
codex:
runner_workflow: .github/workflows/reusable-codex-run.yml
required_secrets:
- CODEX_AUTH_JSON
branch_prefix: codex/issue-
ui_mentions_allowed: false
capabilities:
pr_keepalive: true
pr_autofix: true
belt: true
verifier_checkbox: true
216 changes: 216 additions & 0 deletions .github/scripts/agent_registry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
'use strict';

const fs = require('node:fs');

function stripTrailingComment(rawLine) {
const line = String(rawLine ?? '');
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
return '';
}

// Keep this intentionally simple: our registry YAML should not rely on inline comments.
const match = line.match(/^(.*?)(\s+#.*)?$/);
return (match?.[1] ?? line).replace(/\s+$/, '');
}

function parseScalar(value) {
const raw = String(value ?? '').trim();
if (!raw) {
return '';
}

if (raw === 'true') {
return true;
}
if (raw === 'false') {
return false;
}

if (/^-?\d+$/.test(raw)) {
return Number(raw);
}

const quoted = raw.match(/^(['"])(.*)\1$/);
if (quoted) {
return quoted[2];
}

return raw;
}

function countIndent(line) {
// Match all leading horizontal whitespace (spaces and tabs).
const match = String(line).match(/^([ \t]*)/);
const indentPrefix = match?.[1] ?? '';
if (indentPrefix.includes('\t')) {
throw new Error('Registry YAML must use spaces only (tabs are not allowed)');
}
if (indentPrefix.length % 2 !== 0) {
throw new Error(
`Registry YAML indentation must be multiples of 2 spaces (got ${indentPrefix.length})`,
);
}
return indentPrefix.length;
}

function findNextMeaningfulLine(lines, startIndex) {
for (let index = startIndex; index < lines.length; index += 1) {
const stripped = stripTrailingComment(lines[index]);
if (!stripped.trim()) {
continue;
}
return {
index,
indent: countIndent(stripped),
trimmed: stripped.trim(),
};
}
return null;
}

// Minimal YAML parser for the registry file.
// Supported features:
// - nested mappings via indentation (2 spaces)
// - scalar values (strings, booleans, integers)
// - sequences using "- item" lines (scalar items only)
// Unsupported (intentionally): anchors, multiline strings, flow maps, complex quoting.
function parseRegistryYaml(text) {
const rawLines = String(text ?? '').split(/\r?\n/);
const lines = rawLines.map(stripTrailingComment);

const root = {};
const stack = [{ indent: -1, container: root }];

for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
const rawLine = lines[lineIndex];
if (!rawLine.trim()) {
continue;
}

const indent = countIndent(rawLine);
const trimmed = rawLine.trim();

while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
stack.pop();
}

const parent = stack[stack.length - 1].container;

if (trimmed.startsWith('- ')) {
if (!Array.isArray(parent)) {
throw new Error(`Unexpected list item at line ${lineIndex + 1}; parent is not a list`);
}
parent.push(parseScalar(trimmed.slice(2)));
continue;
}

const sepIndex = trimmed.indexOf(':');
if (sepIndex <= 0) {
throw new Error(`Invalid registry YAML line ${lineIndex + 1}: expected "key: value"`);
}

const key = trimmed.slice(0, sepIndex).trim();
const rest = trimmed.slice(sepIndex + 1).trim();

if (!key) {
throw new Error(`Invalid registry YAML line ${lineIndex + 1}: empty key`);
}
if (typeof parent !== 'object' || parent === null || Array.isArray(parent)) {
throw new Error(`Invalid registry YAML line ${lineIndex + 1}: cannot assign key under a list`);
}

if (rest) {
parent[key] = parseScalar(rest);
continue;
}

const next = findNextMeaningfulLine(lines, lineIndex + 1);
const shouldBeList = Boolean(next && next.indent > indent && next.trimmed.startsWith('- '));
const child = shouldBeList ? [] : {};
parent[key] = child;
stack.push({ indent, container: child });
}

return root;
}

function loadAgentRegistry({ registryPath } = {}) {
const path = registryPath || '.github/agents/registry.yml';
const raw = fs.readFileSync(path, 'utf8');
const registry = parseRegistryYaml(raw);
if (!registry || typeof registry !== 'object') {
throw new Error('Agent registry did not parse into an object');
}
if (!registry.agents || typeof registry.agents !== 'object') {
throw new Error('Agent registry missing required "agents" mapping');
}
if (!registry.default_agent || typeof registry.default_agent !== 'string') {
throw new Error('Agent registry missing required "default_agent" string');
}
return registry;
}

function normalizeLabel(label) {
if (!label) {
return '';
}
if (typeof label === 'string') {
return label.trim();
}
if (typeof label === 'object' && typeof label.name === 'string') {
return label.name.trim();
}
return '';
}

function resolveAgentFromLabels(labels, { registryPath } = {}) {
const registry = loadAgentRegistry({ registryPath });
const labelList = Array.isArray(labels) ? labels : [];
const agentLabels = labelList
.map(normalizeLabel)
.filter(Boolean)
.filter((value) => value.startsWith('agent:'));

const uniqueAgents = new Set(agentLabels.map((value) => value.slice('agent:'.length)));

if (uniqueAgents.size > 1) {
throw new Error(`Multiple agent labels present: ${Array.from(uniqueAgents).join(', ')}`);
}

const explicit = Array.from(uniqueAgents)[0];
const agentKey = explicit || registry.default_agent;
if (!registry.agents[agentKey]) {
const known = Object.keys(registry.agents).sort();
throw new Error(`Unknown agent key: ${agentKey}. Known agents: ${known.join(', ') || '(none)'}`);
}
return agentKey;
}

function getAgentConfig(agentKey, { registryPath } = {}) {
const registry = loadAgentRegistry({ registryPath });
const key = String(agentKey || '').trim() || registry.default_agent;
const config = registry.agents[key];
if (!config) {
const known = Object.keys(registry.agents).sort();
throw new Error(`Unknown agent key: ${key}. Known agents: ${known.join(', ') || '(none)'}`);
}
return config;
}

function getRunnerWorkflow(agentKey, { registryPath } = {}) {
const config = getAgentConfig(agentKey, { registryPath });
const workflow = String(config.runner_workflow || '').trim();
if (!workflow) {
throw new Error(`Agent config missing runner_workflow for agent: ${agentKey}`);
}
return workflow;
}

module.exports = {
getAgentConfig,
getRunnerWorkflow,
loadAgentRegistry,
parseRegistryYaml,
resolveAgentFromLabels,
};
47 changes: 0 additions & 47 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,53 +49,6 @@ When an issue is labeled `agent:codex`:

## Common Issues

## CI Test Policy (PR Gate vs CI vs Release E2E)

This repo intentionally does **not** run the full test surface area on every PR Gate run.

### PR Gate (`.github/workflows/pr-00-gate.yml`)

Goal: fast feedback for most PRs.

- Runs pytest **in parallel** (xdist): `-n auto --dist loadscope`
- Runs pytest **without coverage** (`coverage: false`)
- Skips integration directories:
- `tests/integration/`
- `tests/integrations/`
- Skips **release/packaging** tests via marker: `pytest_markers: "not release"`

These suites will NOT run on PR Gate unless you run them manually (see below).

### Main-branch CI (`.github/workflows/ci.yml`)

Goal: enforce full quality gates on `main`.

- Runs pytest **with coverage** (`coverage: true`, `coverage-min` enforced)
- Runs pytest **in parallel** (xdist): `-n auto --dist loadscope`
- Runs the full test suite (including integration dirs and `release` tests)

### Release/Packaging E2E (`.github/workflows/release-e2e.yml`)

Goal: keep slow PyInstaller + packaged-executable checks out of PR Gate.

- Runs nightly on `main`
- Runs on PRs when the PR is labeled: `run-release`
- Executes only the tests marked `release`: `pytest -m release`

### How to run skipped suites locally

```bash
# Fast PR-gate-like run (parallel, no coverage, skip release + integration dirs)
pytest -q -n auto --dist loadscope -m "not release" \
--ignore=tests/integration --ignore=tests/integrations

# Release / packaging validation (PyInstaller + packaged executable)
pytest -q -m release

# Integration directories (if you need them on a PR)
pytest -q tests/integration tests/integrations
```

### Workflow fails with "workflow file issue"
- A reusable workflow is being called that doesn't exist
- Check Workflows repo has the required `reusable-*.yml` file
Expand Down
1 change: 0 additions & 1 deletion scripts/langchain/issue_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,22 @@
python scripts/langchain/issue_optimizer.py --input-file issue.md --json
"""

from __future__ import annotations

import argparse
import json
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any

from pydantic import BaseModel, ConfigDict, Field

from scripts.langchain.structured_output import (
DEFAULT_REPAIR_PROMPT,
build_repair_callback,
parse_structured_output,
)

Check failure on line 24 in scripts/langchain/issue_optimizer.py

View workflow job for this annotation

GitHub Actions / Python CI / lint-ruff

ruff (I001)

scripts/langchain/issue_optimizer.py:9:1: I001 Import block is un-sorted or un-formatted

Check failure on line 24 in scripts/langchain/issue_optimizer.py

View workflow job for this annotation

GitHub Actions / Python CI / lint-ruff

ruff (I001)

scripts/langchain/issue_optimizer.py:9:1: I001 Import block is un-sorted or un-formatted

try:
from scripts.langchain.injection_guard import check_prompt_injection
Expand Down
1 change: 0 additions & 1 deletion scripts/langchain/pr_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,24 @@
python scripts/langchain/pr_verifier.py --context-file verifier-context.md --json
"""

from __future__ import annotations

import argparse
import json
import logging
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Literal

from pydantic import BaseModel, Field

from scripts import api_client
from scripts.langchain.structured_output import (
build_repair_callback,
parse_structured_output,
)

Check failure on line 26 in scripts/langchain/pr_verifier.py

View workflow job for this annotation

GitHub Actions / Python CI / lint-ruff

ruff (I001)

scripts/langchain/pr_verifier.py:9:1: I001 Import block is un-sorted or un-formatted

Check failure on line 26 in scripts/langchain/pr_verifier.py

View workflow job for this annotation

GitHub Actions / Python CI / lint-ruff

ruff (I001)

scripts/langchain/pr_verifier.py:9:1: I001 Import block is un-sorted or un-formatted

LOGGER = logging.getLogger(__name__)

Expand Down
4 changes: 2 additions & 2 deletions scripts/langchain/progress_reviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ def review_progress_with_llm(
),
feedback_for_agent="Review your recent work against the acceptance criteria.",
summary=(
f"Heuristic review: {len(aligned)}/{len(recent_commits)} commits appear aligned"
f"Heuristic review: {len(aligned)}/" f"{len(recent_commits)} commits appear aligned"
),
used_llm=False,
error="LLM unavailable, using heuristic fallback",
Expand Down Expand Up @@ -454,7 +454,7 @@ def review_progress(
),
feedback_for_agent="Work appears aligned. Continue toward task completion.",
summary=(
f"Heuristic: {len(aligned)}/{len(recent_commits)} commits aligned with criteria"
f"Heuristic: {len(aligned)}/" f"{len(recent_commits)} commits aligned with criteria"
),
used_llm=False,
)
Expand Down
16 changes: 3 additions & 13 deletions scripts/sync_dev_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,6 @@
)


def _is_black_drift(change: str) -> bool:
return change.strip().lower().startswith("black:")


def parse_env_file(path: Path) -> dict[str, str]:
"""Parse the autofix-versions.env file into a dict of key=value pairs."""
if not path.exists():
Expand Down Expand Up @@ -437,22 +433,16 @@ def main(argv: list[str] | None = None) -> int:
return 2

if changes:
if args.check and any(_is_black_drift(change) for change in changes):
print(
"Error: Black formatting pin drift detected (version mismatch/out of sync).",
file=sys.stderr,
)

print(f"{'Applied' if args.apply else 'Found'} {len(changes)} version updates:")
for change in changes:
print(f" - {change}")

if args.check:
print("\nRun with --apply to update dependency files")
return 1

print("\n✓ Dependency files updated")
return 0
else:
print("\n✓ Dependency files updated")
return 0
else:
print("✓ All dev dependency versions are in sync")
return 0
Expand Down
8 changes: 4 additions & 4 deletions tools/requirements-llm.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
# - These are standalone runtime pins for workflow LLM steps, not app deps.
# - When updating, coordinate with requirements.lock and pyproject.toml.
# - Use strict X.Y.Z pins to keep workflow installs reproducible.
langchain==1.2.9
langchain-core==1.2.11
langchain==1.2.10
langchain-core==1.2.13
langchain-community==0.4.1
langchain-openai==1.1.7
langchain-anthropic==1.3.2
langchain-openai==1.1.9
langchain-anthropic==1.3.3
pydantic==2.12.5
requests==2.32.5
Loading