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
20 changes: 19 additions & 1 deletion .github/scripts/gate_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class SummaryContext:
output_path: Path | None
python_required: bool = True
docs_guard_result: str = "success"
test_quality_result: str = "skipped"


@dataclass(slots=True)
Expand Down Expand Up @@ -243,6 +244,7 @@ def _append_job_table(
job_results: Mapping[str, Iterable[str]],
docs_guard_result: str,
docker_result: str,
test_quality_result: str,
) -> None:
lines.append("")
lines.append("| Job | Result |")
Expand All @@ -252,6 +254,7 @@ def _append_job_table(
lines.append(f"| {job_name} | {_friendly(result)} |")
lines.append(f"| docs-guard | {_friendly(docs_guard_result)} |")
lines.append(f"| docker-smoke | {_friendly(docker_result)} |")
lines.append(f"| test-quality | {_friendly(test_quality_result)} |")


def _active_lines(
Expand All @@ -264,9 +267,10 @@ def _active_lines(
job_results: Mapping[str, list[str]],
docs_guard_result: str = "success",
docker_result: str = "skipped",
test_quality_result: str = "skipped",
) -> list[str]:
lines = ["### Gate status", *table]
_append_job_table(lines, job_results, docs_guard_result, docker_result)
_append_job_table(lines, job_results, docs_guard_result, docker_result, test_quality_result)

lint_status, lint_detail = _aggregate(lint_entries)
type_status, type_detail = _aggregate(type_entries)
Expand Down Expand Up @@ -334,13 +338,15 @@ def summarize(context: SummaryContext) -> SummaryResult:
job_results,
docs_guard_result,
context.docker_result,
context.test_quality_result,
)

state = "success"
description = "All Gate checks succeeded."

python_result = _normalize(context.python_result or "success")
docker_result_norm = _normalize(context.docker_result or "skipped")
test_quality_result = _normalize(context.test_quality_result or "skipped")
cosmetic_failure = False
failure_checks: tuple[str, ...] = ()
format_failure = False
Expand Down Expand Up @@ -378,10 +384,20 @@ def summarize(context: SummaryContext) -> SummaryResult:
elif not context.docker_changed:
lines.append("- Docker smoke skipped: no Docker-related changes detected.")

if state == "success":
if test_quality_result == "cancelled":
state = "pending"
description = "Test-quality cancelled; waiting for rerun."
elif test_quality_result not in ("success", "skipped"):
state = "failure"
description = f"Test-quality result: {test_quality_result}."

adjusted_lines = []
for line in lines:
if line.startswith("| docker-smoke"):
adjusted_lines.append(f"| docker-smoke | {_friendly(docker_result_norm)} |")
elif line.startswith("| test-quality"):
adjusted_lines.append(f"| test-quality | {_friendly(test_quality_result)} |")
else:
adjusted_lines.append(line)

Expand Down Expand Up @@ -409,6 +425,7 @@ def build_context() -> SummaryContext:
python_result = os.environ.get("PYTHON_RESULT") or "skipped"
docs_guard_result = os.environ.get("DOCS_GUARD_RESULT") or "success"
docker_result = os.environ.get("DOCKER_RESULT") or "skipped"
test_quality_result = os.environ.get("TEST_QUALITY_RESULT") or "skipped"
docker_changed = _normalize(os.environ.get("DOCKER_CHANGED"), "false") == "true"
python_required = _normalize(os.environ.get("PYTHON_REQUIRED"), "true") == "true"
artifacts_root = Path(os.environ.get("GATE_ARTIFACTS_ROOT", "gate_artifacts"))
Expand All @@ -422,6 +439,7 @@ def build_context() -> SummaryContext:
python_result=python_result,
docs_guard_result=docs_guard_result,
docker_result=docker_result,
test_quality_result=test_quality_result,
docker_changed=docker_changed,
artifacts_root=artifacts_root,
summary_path=summary_path,
Expand Down
137 changes: 137 additions & 0 deletions .github/scripts/runtime_ac_merge_guard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
'use strict';

const RUNTIME_AC_REQUIRED_LABELS = new Set([
'runtime-ac',
'runtime-verification',
'acceptance-criteria',
'verification-spec',
'verification-plan',
'ac-checks',
'runtime-checks',
]);

function labelName(label) {
if (typeof label === 'string') {
return label;
}
if (label && typeof label.name === 'string') {
return label.name;
}
return '';
}

function normalizeLabelName(label) {
return labelName(label).trim().toLowerCase();
}

function runtimeAcRequirement(labels = []) {
const matched = [];
const seen = new Set();

for (const label of labels || []) {
const normalized = normalizeLabelName(label);
if (!normalized) {
continue;
}
const colonIndex = normalized.indexOf(':');
const suffix =
colonIndex >= 0 && colonIndex < normalized.length - 1
? normalized.slice(colonIndex + 1).trim()
: '';
if (
RUNTIME_AC_REQUIRED_LABELS.has(normalized) ||
(suffix && RUNTIME_AC_REQUIRED_LABELS.has(suffix))
) {
if (!seen.has(normalized)) {
matched.push(normalized);
seen.add(normalized);
}
}
}

return {
required: matched.length > 0,
labels: matched,
};
}

function hasRuntimeAcRequirement(labels = []) {
return runtimeAcRequirement(labels).required;
}

// Workflow callers should pass the withRetry function produced by createTokenAwareRetry.
async function fetchPullRequestLabels({ github, owner, repo, prNumber, withRetry }) {
if (!github || !github.rest || !github.rest.issues) {
throw new Error('GitHub client is required to evaluate runtime AC merge labels.');
}
const call = (client = github) =>
client.rest.issues.listLabelsOnIssue({
owner,
repo,
issue_number: prNumber,
per_page: 100,
});

try {
const response = withRetry ? await withRetry(call) : await call();
return Array.isArray(response && response.data) ? response.data : [];
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(
`Unable to evaluate runtime AC merge labels for PR #${prNumber}: ${message}`,
);
}
}

async function assertRuntimeAcMergeAllowed({
github,
core,
owner,
repo,
prNumber,
labels,
withRetry,
source = 'external merge lane',
} = {}) {
if (!owner || !repo || !prNumber) {
throw new Error('owner, repo, and prNumber are required for runtime AC merge guard.');
}

const labelItems = Array.isArray(labels)
? labels
: await fetchPullRequestLabels({ github, owner, repo, prNumber, withRetry });
const requirement = runtimeAcRequirement(labelItems);

if (!requirement.required) {
if (core && typeof core.info === 'function') {
core.info(`Runtime AC merge guard passed for PR #${prNumber}.`);
}
return {
allowed: true,
labels: [],
};
}

const labelList = requirement.labels.join(', ');
const message =
`Runtime AC merge guard blocked ${source} for PR #${prNumber}: ` +
`label(s) ${labelList} require local Orchestrator runtime acceptance checks. ` +
'Merge through Code/Orchestrator/merge_guard.py after the runtime AC spec passes.';

if (core && typeof core.warning === 'function') {
core.warning(message);
}

const error = new Error(message);
error.code = 'runtime_ac_merge_blocked';
error.labels = requirement.labels;
throw error;
}

module.exports = {
RUNTIME_AC_REQUIRED_LABELS,
assertRuntimeAcMergeAllowed,
hasRuntimeAcRequirement,
normalizeLabelName,
runtimeAcRequirement,
};
11 changes: 11 additions & 0 deletions .github/workflows/agents-73-codex-belt-conveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ jobs:
.github/scripts/agent_registry.js
.github/scripts/error_classifier.js
.github/scripts/github-api-with-retry.js
.github/scripts/runtime_ac_merge_guard.js
.github/scripts/token_load_balancer.js
sparse-checkout-cone-mode: false

Expand Down Expand Up @@ -440,6 +441,7 @@ jobs:
script: |
const fs = require('fs');
const retryHelperPath = './.github/scripts/github-api-with-retry.js';
const { assertRuntimeAcMergeAllowed } = require('./.github/scripts/runtime_ac_merge_guard.js');
const retryHelpers = fs.existsSync(retryHelperPath)
? require(retryHelperPath)
: {
Expand All @@ -451,6 +453,15 @@ jobs:
const prNumber = Number('${{ inputs.pr_number }}');
const { owner, repo } = context.repo;
try {
await assertRuntimeAcMergeAllowed({
github,
core,
owner,
repo,
prNumber,
withRetry,
source: 'agents-73-codex-belt-conveyor',
});
await withRetry(() => github.rest.pulls.merge({ owner, repo, pull_number: prNumber, merge_method: 'squash' }));
core.setOutput('merged', 'true');
} catch (error) {
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/agents-81-gate-followups.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1552,6 +1552,7 @@ jobs:
with:
sparse-checkout: |
.github/scripts/github-api-with-retry.js
.github/scripts/runtime_ac_merge_guard.js
.github/scripts/token_load_balancer.js
sparse-checkout-cone-mode: false
- name: Merge labelled agent PRs (guarded)
Expand All @@ -1563,6 +1564,7 @@ jobs:
const fs = require('fs');
const label = 'automerge';
const retryPath = './.github/scripts/github-api-with-retry.js';
const { assertRuntimeAcMergeAllowed } = require('./.github/scripts/runtime_ac_merge_guard.js');
const { createTokenAwareRetry } = fs.existsSync(retryPath)
? require(retryPath)
: {
Expand Down Expand Up @@ -1731,6 +1733,15 @@ jobs:
note = 'Refusing auto-merge: linked issue/PR has unchecked tasks.';
} else {
try {
await assertRuntimeAcMergeAllowed({
github,
core,
owner,
repo,
prNumber,
withRetry,
source: 'agents-81-gate-followups guarded merge',
});
const response = await withRetry((client) => client.rest.pulls.merge({ owner, repo, pull_number: prNumber, merge_method: 'squash' }));
if (response && response.data && response.data.merged) {
status = 'merged';
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/agents-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jobs:
github.event_name == 'pull_request_target' &&
steps.eligibility.outputs.should-run == 'true' &&
steps.api_client_base.outputs.available != 'true'
uses: "stranske/Workflows/.github/actions/setup-api-client@6deed4d3937adab2370b4ddf96046ed295efe68f" # v1
uses: "stranske/Workflows/.github/actions/setup-api-client@d68de1904bcdbe16bfe2462b73aa18f41f8a0a47" # v1
with:
secrets: ${{ toJSON(secrets) }}
github_token: ${{ github.token }}
Expand Down Expand Up @@ -180,7 +180,7 @@ jobs:
steps.eligibility.outputs.should-run == 'true' &&
github.event_name == 'pull_request' &&
steps.api_client_head.outputs.available != 'true'
uses: "stranske/Workflows/.github/actions/setup-api-client@6deed4d3937adab2370b4ddf96046ed295efe68f" # v1
uses: "stranske/Workflows/.github/actions/setup-api-client@d68de1904bcdbe16bfe2462b73aa18f41f8a0a47" # v1
with:
secrets: ${{ toJSON(secrets) }}
github_token: ${{ github.token }}
Expand Down
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

> Read this before changing workflows, prompts, or synced automation files.

## Working Stance — Critical Evaluator (read first)

Your job is correct judgment, not agreement. Evaluate claims, designs, and instructions on the merits before agreeing — including the orchestrator's and the user's. When something is wrong, weaker than an alternative, or missing, say so plainly and lead with the strongest objection. Separate "this is correct" from "I'll do as asked." State your confidence and what would change your mind; flag what you are unsure of. Do not soften a real problem to be agreeable, and do not manufacture disagreement to seem rigorous — calibrated dissent, not maximal.

## This Is A Consumer Repo

Most workflow logic for this repository lives in `stranske/Workflows`. The consumer repo should only carry repo-specific configuration unless it has an explicitly documented exception.
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

> Read this before changing workflows, prompts, or synced automation files.

## Working Stance — Critical Evaluator (read first)

Your job is correct judgment, not agreement. Evaluate claims, designs, and instructions on the merits before agreeing — including the orchestrator's and the user's. When something is wrong, weaker than an alternative, or missing, say so plainly and lead with the strongest objection. Separate "this is correct" from "I'll do as asked." State your confidence and what would change your mind; flag what you are unsure of. Do not soften a real problem to be agreeable, and do not manufacture disagreement to seem rigorous — calibrated dissent, not maximal.

## This Is A Consumer Repo

Most workflow logic for this repository lives in `stranske/Workflows`. The consumer repo should only carry repo-specific configuration unless it has an explicitly documented exception.
Expand Down
Loading
Loading