Skip to content
Closed
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
4 changes: 2 additions & 2 deletions .github/agents/registry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ model_profile_trial_contract:
execution_profiles:
codex-default:
agent: codex
model: gpt-5.5
fallback_model: gpt-5.4
model: gpt-5.6-terra
fallback_model: gpt-5.5
runner: reusable-codex-run
capacity_pool: codex-standard
safety: standard
Expand Down
43 changes: 18 additions & 25 deletions WORKFLOW_USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -781,17 +781,16 @@ The Workflows repository includes maintenance workflows that handle sync, update
---

### `maint-52-sync-dev-versions.yml` - Sync Dev Versions
**Purpose:** Updates development environment versions
**Purpose:** Central Workflows propagation workflow; it has no consumer-template counterpart.

**Trigger:** On push to main or manual
**Trigger:** Runs only in the central Workflows repository after a settled source commit

**What It Does:**
- Syncs Python version from `.python-version`
- Updates Node.js version in workflows
- Updates action versions in workflows
- Commits version bumps
- Uses the central `autofix-versions.env` pin set
- Opens at most one propagation PR per consumer repository
- Records the settled canonical source commit in each delivery marker

**Use When:** After dependency updates
**Use When:** Observe central propagation; do not copy or configure this workflow in a consumer repo

---

Expand Down Expand Up @@ -830,17 +829,16 @@ The Workflows repository includes maintenance workflows that handle sync, update
---

### `maint-auto-update-pypi-versions.yml` - Auto-Update PyPI Packages
**Purpose:** Automatically updates Python package versions
**Purpose:** Central Workflows source-proposal workflow; it has no consumer-template counterpart.

**Trigger:** Daily scheduled
**Trigger:** Monday 03:00 UTC or an explicit central security override

**What It Does:**
- Checks PyPI for latest versions
- Updates minor/patch versions automatically
- Creates PR for major version updates
- Runs CI to validate
- Checks PyPI for the central dev-tool pin set
- Batches routine changes into one mutable weekly source PR
- Runs source validation before the consumer propagation lane can start

**Safety:** Only auto-merges patch versions
**Safety:** Consumer repos must not create partial copies of the central pin update

---

Expand Down Expand Up @@ -1117,20 +1115,15 @@ The Workflows repository includes maintenance workflows that handle sync, update
---

### `maint-50-tool-version-check.yml` - Tool Version Audit
**Purpose:** Checks versions of all development tools
**Purpose:** Central read-only freshness audit; it has no consumer-template counterpart.

**Trigger:** Weekly scheduled
**Trigger:** Weekly in the central Workflows repository

**Checks:**
- Python version
- Node.js version
- pip version
- git version
- gh version
- docker version
- Action versions

**Result:** Report on outdated tools
- PyPI freshness for the central developer-tool pin set (`black`, `ruff`, `mypy`, `pytest`, related tooling)
- Canonical pin alignment evidence only (no runtime/CLI/Action version inventory)

**Result:** Freshness evidence only; it never creates a competing update issue or PR

---

Expand Down
59 changes: 59 additions & 0 deletions scripts/check_deliberate_break.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
r"\b(assert|expect\(|pytest\.raises\(|assert\.)\b",
)
DEFAULT_TIMEOUT_SECONDS = 120
PYTEST_RUNTIME_DEPENDENCIES = ("pyyaml==6.0.3",)


@dataclass(frozen=True)
Expand Down Expand Up @@ -130,6 +131,33 @@ def _pytest_command(test_id: str) -> tuple[str, ...]:
return (sys.executable, "-m", "pytest", test_id, "-q")


def _ensure_pytest_runtime_deps() -> None:
"""Install lightweight deps Gate test-quality may not preinstall.

Gate's test-quality job installs only ``pytest``. Deliberate-break may still
collect tests that import PyYAML (for example via ``sync_manifest_compiler``).
Installing here avoids editing ``pr-00-gate.yml``, which forces an
Actions ``action_required`` approval wait on workflow-touching PRs.
"""
try:
import yaml # noqa: F401
except ImportError:
subprocess.run(
[
sys.executable,
"-m",
"pip",
"install",
"--upgrade",
*PYTEST_RUNTIME_DEPENDENCIES,
],
check=True,
text=True,
capture_output=True,
timeout=DEFAULT_TIMEOUT_SECONDS,
)


def _run(
command: tuple[str, ...],
cwd: Path,
Expand Down Expand Up @@ -244,6 +272,37 @@ def verify_spec(
changed_assertions=tampered,
)

except subprocess.TimeoutExpired as exc:
return _json_result(
VERDICT_BROKEN,
reason="command-timeout",
command=list(exc.cmd) if isinstance(exc.cmd, (tuple, list)) else str(exc.cmd),
Comment on lines +275 to +279
timeout=exc.timeout,
)

try:
_ensure_pytest_runtime_deps()
except subprocess.TimeoutExpired as exc:
return _json_result(
VERDICT_BROKEN,
reason="command-timeout",
command=list(exc.cmd) if isinstance(exc.cmd, (tuple, list)) else str(exc.cmd),
timeout=exc.timeout,
)
except subprocess.CalledProcessError as exc:
return _json_result(
VERDICT_BROKEN,
reason="dependency-install-failed",
detail=exc.stderr or str(exc),
)
except OSError as exc:
return _json_result(
VERDICT_BROKEN,
reason="dependency-install-unavailable",
detail=str(exc),
)

try:
head_run = _run(spec.command, repo)
except subprocess.TimeoutExpired as exc:
return _json_result(
Expand Down
47 changes: 42 additions & 5 deletions scripts/langchain/pr_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -675,12 +675,39 @@ def _fallback_evaluation(
)


def _text_from_response_content(content: object) -> str | None:
"""Return provider text, or None when the payload carries no text blocks."""
if isinstance(content, str):
return content
if isinstance(content, list):
text_blocks = [
block["text"]
for block in content
if isinstance(block, dict) and isinstance(block.get("text"), str)
]
if text_blocks:
# Concatenate without a separator: a provider may split one JSON
# document across blocks, and an inserted newline inside a string
# literal would make the reassembled payload invalid JSON.
return "".join(text_blocks)
return None


def _coerce_response_content(content: object) -> str:
"""Return text from provider response blocks without losing a safe fallback."""
text = _text_from_response_content(content)
if text is not None:
return text
return json.dumps(content, default=str)


def _parse_llm_response(
content: str, provider: str, *, client: object | None = None
content: object, provider: str, *, client: object | None = None
) -> EvaluationResult:
content_text = _coerce_response_content(content)
repair = _build_verifier_repair_callback(client) if client is not None else None
parsed = parse_structured_output(
content,
content_text,
EvaluationPayload,
repair=repair,
max_repair_attempts=SCHEMA_REPAIR_POLICY.max_attempts,
Expand All @@ -704,7 +731,7 @@ def _parse_llm_response(
summary=None,
provider_used=provider,
used_llm=True,
raw_content=content,
raw_content=content_text,
error=error,
)

Expand All @@ -717,19 +744,29 @@ def _parse_llm_response(
summary=payload.summary,
provider_used=provider,
used_llm=True,
raw_content=parsed.raw_content or content,
raw_content=parsed.raw_content or content_text,
)


def _build_verifier_repair_callback(client: object) -> Callable[[str, str, str], str | None]:
repair = build_repair_callback(client)

def _repair(schema_json: str, validation_errors: str, raw_response: str) -> str | None:
return repair(
repaired = repair(
schema_json,
validation_errors,
_cap_prompt_text(raw_response, EVAL_SCHEMA_REPAIR_BUDGET_TOKENS),
)
if not repaired:
return None
# The repair path must be stricter than the parse path. A reply of only
# thinking/metadata blocks, or of empty text blocks, is still truthy, and
# serializing it would hand the parser block metadata dressed up as a
# repair attempt — burning the one retry on noise.
text = _text_from_response_content(repaired)
if text is None or not text.strip():
return None
return text

return _repair

Expand Down
Loading