Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8436029
fix(sandbox): separate trusted result evidence from stdout
seonghobae Sep 11, 2026
1ca7658
test(sandbox): require binary-safe trusted evidence bundle
seonghobae Sep 12, 2026
74d0154
fix(sandbox): harden trusted result bundle handoff
seonghobae Sep 12, 2026
cc545fd
test(queue): reject draft required-workflow admission
seonghobae Sep 12, 2026
b57a531
fix(queue): skip heavy required jobs on Draft PRs
seonghobae Sep 12, 2026
a4298cf
fix(sandbox): preserve causal failure status
seonghobae Sep 12, 2026
ca9311a
fix(git-data): restore complete queue contract blobs
seonghobae Sep 12, 2026
e64fb47
test(queue): preserve complete Draft-admission RED
seonghobae Sep 12, 2026
228116d
fix(queue): restore guarded exact-tree GREEN
seonghobae Sep 12, 2026
d960c67
merge main into sandbox result handoff repair
seonghobae Sep 12, 2026
de7c1ca
test(queue): cover Draft gitleaks admission
seonghobae Sep 12, 2026
2df3dbb
fix(queue): skip Draft gitleaks admission
seonghobae Sep 12, 2026
fdf8555
test(queue): prove Runtime Quality Ready re-entry RED
seonghobae Sep 12, 2026
719af93
fix(queue): reenter Runtime Quality on Ready
seonghobae Sep 12, 2026
863c214
docs(queue): record complete Draft admission boundary
seonghobae Sep 12, 2026
f9c7a04
fix(sandbox): preserve forwarded output order
seonghobae Sep 12, 2026
0c0e73d
style(queue): normalize Runtime Ready contract EOF
seonghobae Sep 12, 2026
bb42e0c
test(sandbox): keep envelope hidden until streams complete
seonghobae Sep 12, 2026
5a85d0a
test(sandbox): require atomic envelope publication
seonghobae Sep 12, 2026
eda9319
fix(sandbox): publish completed envelope atomically
seonghobae Sep 12, 2026
f111da9
test(sandbox): cover atomic publication edges
seonghobae Sep 12, 2026
5e768ac
test(runtime): require sandbox evidence path admission
seonghobae Sep 12, 2026
ad6fec5
fix(runtime): admit sandbox evidence changes
seonghobae Sep 12, 2026
3648b84
test(runtime): reject vacuous sandbox path admission
seonghobae Sep 12, 2026
dc4291f
merge: integrate atomic sandbox evidence into runtime owner
seonghobae Sep 12, 2026
d147388
fix(runtime): execute sandbox evidence suite
seonghobae Sep 12, 2026
b671555
test(runtime): reject vacuous trigger slice
seonghobae Sep 12, 2026
2c00900
fix(runtime): slice exact trigger block
seonghobae Sep 12, 2026
cc6e39f
docs(runtime): bind non-vacuous sandbox admission
seonghobae Sep 12, 2026
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: 4 additions & 0 deletions .github/workflows/agent-review-runtime-quality-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ name: Agent Review Runtime Quality CI

on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches: [main]
paths:
- ".github/workflows/agent-review-runtime-quality-ci.yml"
- "scripts/ci/sandboxed_verify.py"
- "tests/test_sandboxed_verify.py"
- ".github/workflows/noema-review.yml"
- ".github/actions/noema-review/two_phase.py"
- "tests/test_noema_reviewer_token_lifetime.py"
Expand Down Expand Up @@ -116,6 +119,7 @@ permissions:
jobs:
agent_review_runtime_quality:
name: agent-review-runtime-quality
if: github.event.pull_request.draft == false
runs-on: ubuntu-24.04
timeout-minutes: 25
env:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/codeql-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ permissions:
jobs:
detect-languages:
name: Detect CodeQL languages
if: github.event.action != 'closed'
if: github.event.action != 'closed' && github.event.pull_request.draft == false
runs-on: ubuntu-24.04
permissions:
contents: read
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/python-security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ permissions:
jobs:
detect-python:
name: Detect Python
if: github.event.action != 'closed'
if: >-
github.event.action != 'closed' &&
(github.event_name != 'pull_request' || github.event.pull_request.draft == false)
runs-on: ubuntu-24.04
outputs:
has_python: ${{ steps.detect.outputs.has_python }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sast-semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
# here and consumed through `needs`. See
# docs/doctoring/required-workflow-path-filter-boundary.md.
# Fails OPEN: an unreadable, empty, or truncated file list scans everything.
if: github.event.action != 'closed'
if: github.event.action != 'closed' && (github.event_name != 'pull_request' || github.event.pull_request.draft == false)
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/security-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
# here and consumed through `needs`. See
# docs/doctoring/required-workflow-path-filter-boundary.md.
# Fails OPEN: an unreadable, empty, or truncated file list scans everything.
if: github.event.action != 'closed'
if: github.event.action != 'closed' && github.event.pull_request.draft == false
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
Expand Down Expand Up @@ -431,7 +431,7 @@ jobs:
# push, schedule, and manual backstops remain in secret-scan.yml.
gitleaks:
name: gitleaks (secret scan)
if: github.event.action != 'closed' && github.repository == 'ContextualWisdomLab/.github'
if: github.event.action != 'closed' && github.event.pull_request.draft == false && github.repository == 'ContextualWisdomLab/.github'
runs-on: ubuntu-24.04
permissions:
contents: read
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ this file. The format follows Keep a Changelog, and versioned releases follow
Semantic Versioning where the repository publishes a release.

## [Unreleased]
- **Stop Draft PR pushes from consuming five required-workflow runner lanes.**
Every independent entry job in Runtime Quality, CodeQL, Security Scan
(including its document-sensitive Gitleaks gate), Python Security, and SAST
now skips while a pull request is Draft. Existing
`ready_for_review` triggers create fresh exact-head evidence after review
admission; Runtime Quality now explicitly subscribes to that event as well.
Push, schedule, and repository-dispatch coverage remains intact.
A contract pins both pull-request-only and mixed-event guards.
- **Pin `opencode-review-dispatch.yml` off the starved floating `ubuntu-latest` image.**
The 2026-09-01 floating-image fix (see that entry below) pinned `strix.yml`,
`opencode-review.yml`, and `noema-review.yml` -- the three required-check
Expand Down
55 changes: 55 additions & 0 deletions docs/doctoring/draft-required-workflow-admission-20260912.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Draft required-workflow admission

## Problem and exact evidence

Pull request #2106 head `24bb6591ab7df23558cb793b4af60c567ff9da97`
generated Security Scan `34688578677`, CodeQL PR `34688578675`, SAST
`34688578674`, Python Security `34688578683`, and Runtime Quality
`34688578679` while the pull request was Draft. Restoring Ready at the same
head generated a second set. The first four runs were cancelled after queued
jobs had already entered admission; Runtime Quality had already consumed a
runner and completed. This is same-head lifecycle queue waste, not stale source
or a test failure.

The first successor canary exposed two independent omissions. Security Scan
skipped its `changed-scope` path but admitted independent Gitleaks job
`103542086113`. After that guard was repaired, Ready restoration generated four
security workflows but no Runtime Quality run because that workflow relied on
the default `pull_request` activity set, which excludes `ready_for_review`.

## Constraints and selected repair

The workflows must keep `ready_for_review`, PR-keyed concurrency, and their
existing close-event behavior. Security workflows that also run on push,
schedule, or `repository_dispatch` must not lose those non-PR paths. Trigger
filters alone are insufficient for organization required workflows, so the
repair uses the existing job-level policy boundary:

- pull-request-only workflows require `pull_request.draft == false` on every
independent entry job, including both Security Scan `changed-scope` and its
document-sensitive `gitleaks` gate;
- mixed-event workflows allow every non-PR event and require non-Draft state
only for pull-request events;
- downstream jobs remain unchanged and naturally skip through `needs` when the
admission job skips.
- Runtime Quality explicitly subscribes to `ready_for_review`, so its Draft
skip cannot strand the exact head when review admission opens.

No new workflow, dependency, scheduler, token, or status context is added.

## Alternatives rejected

- Removing `ready_for_review` would strand Draft-origin PRs without fresh
evidence when they become reviewable.
- Adding head SHA to concurrency would not prevent the same-head lifecycle
duplication and would weaken close-event cancellation.
- Cancelling the duplicate later still spends queue admission and runner time.

## Verification and follow-up

`tests/test_required_workflow_queue_contract.py` binds all five workflows and
all independent entry-job guards while preserving the existing close-event
contract and requiring Runtime Quality Ready admission. The proposal is not
complete until exact-head hosted Checks and independent review pass, it merges
through ordinary protection, and a post-merge Draft→Ready canary shows skipped
Draft jobs followed by one fresh Ready generation.
1 change: 1 addition & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ flowchart LR

| Gap ID | 현재 관측 | 구매자 영향 | 우선 구현/검증 |
|---|---|---|---|
| GAP-DRAFT-RUNNER-ADMISSION | 같은 exact head의 Draft push가 다섯 required workflow runner lane을 먼저 점유하고, Ready 전환이 그 generation을 취소한 뒤 새 generation을 만들었다. 첫 수리 canary에서는 Security Scan의 독립 `gitleaks` job만 여전히 runner queue에 진입했고, 그 guard 수리 뒤에는 Runtime Quality가 `ready_for_review`를 구독하지 않아 Ready generation 자체가 누락됐다. | 검토할 수 없는 Proposed 변경이 scarce runner capacity를 소모하거나 Ready exact head에 필수 품질 evidence가 영구 누락돼 merge-ready PR의 검증을 지연한다. | 모든 독립 entry job에서 Draft를 job-level skip하고 `ready_for_review`에서 fresh exact-head Checks를 생성한다. Security Scan의 `changed-scope`와 document-sensitive `gitleaks`를 모두 포함하고 Runtime Quality trigger를 명시하며 mixed-event workflow의 push/schedule/dispatch는 유지한다. Proposed successor에서 RED→GREEN 및 hosted evidence를 검증한다. |
| G-01 | 열린 PR은 107개다. metadata 상태는 BLOCKED=17, BEHIND=16, DIRTY=74, draft 13개다. 상태는 independent exact-head approval과 terminal required Checks를 자동으로 의미하지 않는다 | 안전하게 출시할 변경과 대기 중인 변경을 구별할 수 없다 | PR마다 current head, reviews, threads, required Checks, merge-result tree를 재수집하고 보호 조건 미충족이면 merge하지 않는다 |
| G-02 | protected `main`은 `826b92394c63deb6981c3a8d16a724d71f85a0d7`이며, BEHIND/stacked PR의 predecessor evidence를 current-head approval로 승격할 수 없다 | 리뷰가 호출돼도 승인 증거가 생성되지 않아 자동화가 멈춘다 | current-head quality와 OpenCode/Noema/Strix를 재실행하고, exact SHA·run ID·review commit SHA를 한 receipt에 묶는다 |
| G-03 | #1297은 Strix per-repository serialization과 scoped close cleanup을, #1345/#1347은 normalizer/web-E2E 안전성을 다룬다. 각 PR의 provider failure와 source/control-plane failure를 구분해야 한다 | 취약점 0건이어도 CI 인프라 결함이 보안 결과처럼 보이고 큐가 막힌다 | D3 교착 증거를 별도 수집하고, vulnerability marker는 절대 neutralize하지 않으며, 정상 gate 복구 후 exact-head hosted evidence를 재생성한다 |
Expand Down
51 changes: 51 additions & 0 deletions tests/test_required_workflow_queue_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -1058,6 +1058,41 @@ def test_pr_keyed_scan_workflows_pin_cancellation_as_a_value() -> None:
assert "github.event_name" not in group_value


def test_required_heavy_jobs_wait_until_pull_request_is_ready() -> None:
"""Draft pushes must not consume runners before review admission."""
pull_request_only_jobs = (
("agent-review-runtime-quality-ci.yml", "agent_review_runtime_quality"),
("codeql-pr.yml", "detect-languages"),
("security-scan.yml", "changed-scope"),
("security-scan.yml", "gitleaks"),
)
mixed_event_jobs = {
"python-security.yml": "detect-python",
"sast-semgrep.yml": "changed-scope",
}

for filename, job_name in pull_request_only_jobs:
workflow = workflow_text(filename)
job_match = re.search(
rf"(?ms)^ {re.escape(job_name)}:\n(.*?)(?=^ [a-zA-Z0-9_-]+:\s*$|\Z)",
workflow,
)
assert job_match is not None
job = job_match.group(1)
assert "github.event.pull_request.draft == false" in job

for filename, job_name in mixed_event_jobs.items():
workflow = workflow_text(filename)
job_match = re.search(
rf"(?ms)^ {re.escape(job_name)}:\n(.*?)(?=^ [a-zA-Z0-9_-]+:\s*$|\Z)",
workflow,
)
assert job_match is not None
job = job_match.group(1)
assert "github.event_name != 'pull_request'" in job
assert "github.event.pull_request.draft == false" in job


def test_pull_request_close_events_cancel_superseded_runs_without_heavy_jobs() -> None:
"""Close events should cancel old runs without starting expensive jobs."""
workflows = (
Expand Down Expand Up @@ -2103,3 +2138,19 @@ def test_scorecard_medium_plus_governance_has_owner_and_runbook() -> None:
assert "latest head commit" in runbook
assert "cancel superseded runs" in runbook
assert "Every central workflow failure must print the actionable reason" in runbook

def test_runtime_quality_reenters_when_draft_becomes_ready() -> None:
"""A same-head Ready transition must create fresh Runtime Quality evidence."""
workflow = workflow_text("agent-review-runtime-quality-ci.yml")
trigger = workflow.split("\nconcurrency:", 1)[0]

assert "types: [opened, synchronize, reopened, ready_for_review]" in trigger


def test_runtime_quality_admits_sandbox_evidence_changes() -> None:
"""Sandbox evidence changes must trigger the Runtime Quality gate."""
workflow = workflow_text("agent-review-runtime-quality-ci.yml")
trigger = workflow.split("\\nconcurrency:", 1)[0]

assert '- "scripts/ci/sandboxed_verify.py"' in trigger
assert '- "tests/test_sandboxed_verify.py"' in trigger
Loading