From 191e2e36de700c75709a6e6834cfeb10cc416f9e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 15:36:34 +0900 Subject: [PATCH 01/20] feat(automation): run LineageWeave hourly NVIDIA NIM review repair Add a thin minute-4 caller for ContextualWisdomLab/LineageWeave on protected main. The caller maps only established scheduler credentials, grants job-scoped OIDC, and leaves NVIDIA_NIM_API_KEY on the reusable worker. --- .../hourly-nvidia-nim-review-repair.yml | 31 ++++ .../lineageweave-hourly-review-repair.yml | 34 ++++ CHANGELOG.md | 1 + .../lineageweave-hourly-review-caller.md | 141 +++++++++++++++ .../test_lineageweave_hourly_review_caller.py | 166 ++++++++++++++++++ 5 files changed, 373 insertions(+) create mode 100644 .github/workflows/lineageweave-hourly-review-repair.yml create mode 100644 docs/doctoring/lineageweave-hourly-review-caller.md create mode 100644 tests/test_lineageweave_hourly_review_caller.py diff --git a/.github/workflows/hourly-nvidia-nim-review-repair.yml b/.github/workflows/hourly-nvidia-nim-review-repair.yml index db7224f5a..c0781c37e 100644 --- a/.github/workflows/hourly-nvidia-nim-review-repair.yml +++ b/.github/workflows/hourly-nvidia-nim-review-repair.yml @@ -25,6 +25,17 @@ on: - tests/test_hourly_scheduler_runtime_budget.py - tests/test_nonnest2_hourly_review_caller.py - tests/test_originweave_hourly_review_caller.py + - .github/workflows/clearfolio-hourly-review-repair.yml + - .github/workflows/disksage-hourly-review-repair.yml + - .github/workflows/fast-mlsirm-hourly-review-repair.yml + - .github/workflows/hourly-nvidia-nim-review-repair.yml + - .github/workflows/lineageweave-hourly-review-repair.yml + - scripts/ci/pr_review_conflict_scope.py + - scripts/ci/pr_review_autofix_context.py + - tests/test_disksage_hourly_review_caller.py + - tests/test_fast_mlsirm_hourly_review_caller.py + - tests/test_hourly_scheduler_runtime_budget.py + - tests/test_lineageweave_hourly_review_caller.py - tests/test_hourly_autofix_context_quality_gate.py - tests/test_pr_review_conflict_scope.py - tests/test_pr_review_conflict_scope_control_files.py @@ -48,6 +59,8 @@ on: - docs/doctoring/hourly-nvidia-nim-autofix.md - docs/doctoring/nonnest2-hourly-review-caller.md - docs/doctoring/originweave-hourly-review-caller.md + - docs/doctoring/hourly-nvidia-nim-autofix.md + - docs/doctoring/lineageweave-hourly-review-caller.md push: paths: - .github/workflows/pr-review-fix-scheduler.yml @@ -72,6 +85,17 @@ on: - tests/test_hourly_scheduler_runtime_budget.py - tests/test_nonnest2_hourly_review_caller.py - tests/test_originweave_hourly_review_caller.py + - .github/workflows/clearfolio-hourly-review-repair.yml + - .github/workflows/disksage-hourly-review-repair.yml + - .github/workflows/fast-mlsirm-hourly-review-repair.yml + - .github/workflows/hourly-nvidia-nim-review-repair.yml + - .github/workflows/lineageweave-hourly-review-repair.yml + - scripts/ci/pr_review_conflict_scope.py + - scripts/ci/pr_review_autofix_context.py + - tests/test_disksage_hourly_review_caller.py + - tests/test_fast_mlsirm_hourly_review_caller.py + - tests/test_hourly_scheduler_runtime_budget.py + - tests/test_lineageweave_hourly_review_caller.py - tests/test_hourly_autofix_context_quality_gate.py - tests/test_pr_review_conflict_scope.py - tests/test_pr_review_conflict_scope_control_files.py @@ -95,6 +119,8 @@ on: - docs/doctoring/hourly-nvidia-nim-autofix.md - docs/doctoring/nonnest2-hourly-review-caller.md - docs/doctoring/originweave-hourly-review-caller.md + - docs/doctoring/hourly-nvidia-nim-autofix.md + - docs/doctoring/lineageweave-hourly-review-caller.md permissions: contents: read @@ -150,6 +176,10 @@ jobs: tests/test_hourly_scheduler_runtime_budget.py \ tests/test_nonnest2_hourly_review_caller.py \ tests/test_originweave_hourly_review_caller.py \ + tests/test_disksage_hourly_review_caller.py \ + tests/test_fast_mlsirm_hourly_review_caller.py \ + tests/test_hourly_scheduler_runtime_budget.py \ + tests/test_lineageweave_hourly_review_caller.py \ tests/test_pr_review_conflict_scope_control_files.py \ tests/test_hourly_autofix_context_quality_gate.py \ tests/test_pr_review_conflict_scope_git_executable.py \ @@ -162,3 +192,4 @@ jobs: tests/test_pr_review_autofix_nvidia_nim_contract.py \ tests/test_pr_review_autofix_writer_security_contract.py git diff --check + git diff --check \ No newline at end of file diff --git a/.github/workflows/lineageweave-hourly-review-repair.yml b/.github/workflows/lineageweave-hourly-review-repair.yml new file mode 100644 index 000000000..9d06d5c93 --- /dev/null +++ b/.github/workflows/lineageweave-hourly-review-repair.yml @@ -0,0 +1,34 @@ +name: LineageWeave Hourly Review Repair + +on: + schedule: + # Minute 4 avoids pg-llm-batch (1), aFIPC (2), kaefa (3), codec-carver (5), + # Wardnet (7), naruon (11), pg-erd-cloud (13), orchestrator (17), + # noema (19), Clearfolio (23), Keyverse (29), Scopeweave (31), + # DiskSage (37), Appguardrail (41), newsdom-api (43), Inkspan (47), + # fast-mlsirm (49), BandScope (53), and semantic-data-portal (59). + - cron: "4 * * * *" + +concurrency: + group: lineageweave-hourly-review-repair + # A later heartbeat must not cancel an in-flight lineage or linkage RCA. + cancel-in-progress: false + +permissions: + contents: read + +jobs: + dispatch-review-repair: + permissions: + contents: read + id-token: write + uses: ./.github/workflows/pr-review-fix-scheduler.yml + with: + target_repository: ContextualWisdomLab/LineageWeave + base_branch: main + max_prs: "50" + max_dispatches: "1" + retry_hours: "2" + secrets: + PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} + OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bf8ad766..a8a48078e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Semantic Versioning where the repository publishes a release. - Added an hourly organization commercial-readiness coordinator that discovers writable repositories, honors enabled dedicated writer leases and fully paginated live writer runs, refetches exact repository/workflow/run/PR state before dispatch, rotates bounded review-repair and opt-in NVIDIA OpenCode product-development targets, fails nonzero on fleet-wide inspection or dispatch outages, retains three-day JSON receipts, and keeps the existing 15-minute merge scheduler authoritative. - Added a dedicated OriginWeave hourly caller that invokes the product-neutral central scheduler with the exact repository, protected `main` branch, one-dispatch budget, two-hour same-head retry floor, non-cancelling single-flight heartbeat, job-scoped OIDC, and only the established scheduler credentials. +- Added a dedicated LineageWeave hourly caller that invokes the product-neutral central scheduler with the exact repository, protected `main` branch, one-dispatch budget, two-hour same-head retry floor, non-cancelling single-flight heartbeat, job-scoped OIDC, and only the established scheduler credentials. - Added a trusted pull-request comment router for `@cwl-noema-review` and review-only `@opencode-agent` dispatches, with an organization sweep, exact-head receipts, repository allowlisting, fixed runners, immutable checkout pins, and a permanent 100% statement/branch/docstring quality gate. - Added exact-base `uv.lock` materialization that reconstructs standalone nested projects with a checksum-pinned official `uv` exporter, isolated frozen/offline execution, strict exact-pin and SHA-256 output validation, and complete Python 3.10/3.14 quality evidence. - Added a permanent exact-head contract workflow for the hourly review-repair scheduler, immutable reusable-workflow source, NVIDIA NIM model boundary, credential isolation, and fail-closed unattended-agent permissions. diff --git a/docs/doctoring/lineageweave-hourly-review-caller.md b/docs/doctoring/lineageweave-hourly-review-caller.md new file mode 100644 index 000000000..432e4b18c --- /dev/null +++ b/docs/doctoring/lineageweave-hourly-review-caller.md @@ -0,0 +1,141 @@ +# LineageWeave hourly review-repair caller + +검토 기준일: **2026-08-17** + +## Decision + +ContextualWisdomLab operates one protected hourly caller for +`ContextualWisdomLab/LineageWeave` (git-branch-style lineage DAGs from +scattered short records: multi-channel score fusion, LLM adjudication, +and Keyverse RP affiliation). The caller runs at minute 4, delegates to +the product-neutral central review-fix scheduler, inspects at most 50 +open pull requests targeting protected `main`, and dispatches at most +one bounded repair per heartbeat. + +A paying buyer of reconstructed affiliation lineage would feel live +LineageWeave pull requests stalling while hourly NVIDIA NIM repair +scanned only Clearfolio, DiskSage, and fast-mlsirm. Live heads such as +ContextualWisdomLab/LineageWeave#192 (plural affiliation next action), +ContextualWisdomLab/LineageWeave#190 (entity-resolution ADR), +ContextualWisdomLab/LineageWeave#123 (plural affiliation chips), and +ContextualWisdomLab/LineageWeave#74 (PROV-O actor lineage) target `main` +and never enter those other callers. + +The caller does not implement review or mutation logic itself. +LineageWeave remains standalone; Keyverse and naruon federate through +its RP profile without owning the lineage runtime. Privileged automation +stays in `ContextualWisdomLab/.github`. + +## Root-cause analysis and remediation feasibility + +The reusable worker performs exact-head root-cause analysis and tests +remediation feasibility before it edits. The reusable worker must: + +1. Refetch the exact live head, base, reviews, checks, changed paths, and + writer state. +2. Establish the causal chain rather than repeat the terminal symptom. +3. Enumerate materially distinct minimal remedies. +4. Reject remedies that lack writer authority, cross sealed paths, require + unavailable credentials or protected-setting changes, violate stack + order, cannot be verified, or do not alter the diagnosed cause. +5. Dispatch at most one feasible repair. Otherwise leave the tree + unchanged. + +A queued or pending check remains a merge blocker but is not itself a +code finding. The independent non-author approval remains an external +authorization gate and is never synthesized by the repair worker. The +worker cannot approve, merge, release, resolve review findings by +inference, change protection, or manufacture passing checks. + +## Cadence and concurrency + +The caller uses a single concurrency group and `cancel-in-progress: false`. +This preserves an in-flight bounded RCA instead of discarding lineage +evidence when the next hourly heartbeat arrives. The reusable scheduler +cancels only its own superseded short queue scan. + +The caller sets a **two-hour same-head retry floor**. Central OpenCode and +NVIDIA NIM work, plus entity-resolution or affiliation analysis, can +legitimately approach two hours. An hourly redispatch of the same +unchanged head would create duplicate writer pressure rather than faster +remediation. + +GitHub scheduled workflows can be delayed under load and execute only +from the default branch. The cron expression is a heartbeat, not a +real-time SLA. + +## Credential and model boundary + +The caller keeps workflow `GITHUB_TOKEN` at `contents: read` and grants +the reusable job `id-token: write` so the central scheduler can mint the +OpenCode GitHub App token from GitHub OIDC when the mapped PAT is absent +(GitHub, n.d.-c). It maps only `PR_REVIEW_MERGE_TOKEN` and +`OPENCODE_APPROVE_TOKEN`. It never uses `secrets: inherit`, receives +`NVIDIA_NIM_API_KEY`, or introduces `COPILOT_GITHUB_TOKEN`. CWE-250 +forbids executing the caller with write or model privileges it does not +need (MITRE, 2026). + +Model execution remains inside the central worker. The model credential +is the GitHub Secret `NVIDIA_NIM_API_KEY`; the caller does not receive or +forward it. + +Before protected-main activation, the repository variable +`OPENCODE_REPOSITORY_DISPATCH_TARGETS` must contain the exact +`ContextualWisdomLab/LineageWeave` target. Missing or mismatched +configuration fails before mutation credential materialization. + +## Security, standalone operation, and modularity + +The caller adds no LineageWeave runtime dependency, database object, +network endpoint, tenant authority, or product credential. LineageWeave +continues to run as a standalone lineage reconstructor. Keyverse, naruon, +and other CWL services may consume its affiliation evidence, but they +cannot weaken its exact-head, approval, or security gates. + +## Verification and rollback + +Machine-checkable contracts require the exact target/base, minute 4 +cadence, non-cancelling single-flight group, one dispatch, two-hour +retry floor, explicit secret mapping, read-only contents plus job-scoped +`id-token: write`, focused path-filter coverage, and absence of model or +Copilot credentials. Independent `pull_request`, `push`, and `compileall` +path blocks must each name the caller, doctoring, or contract they own. + +After source integration, closure requires a scheduled or manual +protected-main consumer run proving the exact LineageWeave repository and +`main` base. Source checks alone are not protected-main operational acceptance. +Merge still requires zero unresolved valid findings and a +qualifying independent non-author approval. + +Rollback removes the LineageWeave caller, its focused test, doctoring, and +central path-filter/documentation entries. It must not remove scheduler +dispatch validation or affect independent product callers. + +## APA 7th references + +GitHub, Inc. (n.d.-a). *Events that trigger workflows*. GitHub Docs. +Retrieved August 17, 2026, from +https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule + +GitHub, Inc. (n.d.-b). *Reuse workflows*. GitHub Docs. Retrieved August +17, 2026, from +https://docs.github.com/en/actions/how-tos/sharing-automations/reuse-workflows + +GitHub, Inc. (n.d.-c). *Automatic token authentication*. GitHub Docs. +Retrieved August 17, 2026, from +https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token + +MITRE. (2026). *CWE-250: Execution with unnecessary privileges*. +https://cwe.mitre.org/data/definitions/250.html + +National Institute of Standards and Technology. (2022). *Secure software +development framework (SSDF) version 1.1: Recommendations for mitigating +the risk of software vulnerabilities* (NIST Special Publication 800-218). +https://doi.org/10.6028/NIST.SP.800-218 + +NVIDIA. (n.d.). *NVIDIA NIM for large language models documentation*. +Retrieved August 17, 2026, from +https://docs.nvidia.com/nim/large-language-models/latest/ + +OpenCode. (n.d.). *OpenCode documentation*. Retrieved August 17, 2026, +from https://opencode.ai/docs/ diff --git a/tests/test_lineageweave_hourly_review_caller.py b/tests/test_lineageweave_hourly_review_caller.py new file mode 100644 index 000000000..c7ccb25f3 --- /dev/null +++ b/tests/test_lineageweave_hourly_review_caller.py @@ -0,0 +1,166 @@ +"""Contract tests for LineageWeave's bounded hourly review-repair caller.""" + +from pathlib import Path + + +CALLER = Path(".github/workflows/lineageweave-hourly-review-repair.yml") +DOCTORING = Path("docs/doctoring/lineageweave-hourly-review-caller.md") +QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") +SCHEDULER = Path(".github/workflows/pr-review-fix-scheduler.yml") + + +def _read(path: Path) -> str: + """Return one repository contract file as UTF-8 text.""" + return path.read_text(encoding="utf-8") + + +def _yaml_path_entries(block: str) -> set[str]: + """Return dashed YAML path entries from one trigger or compileall block.""" + entries: set[str] = set() + for raw_line in block.splitlines(): + stripped = raw_line.strip() + if stripped.startswith("- "): + entries.add(stripped[2:].strip()) + elif stripped.startswith("tests/") or stripped.startswith("scripts/"): + entries.add(stripped.rstrip(" \\")) + return entries + + +def _trigger_path_block(quality: str, trigger: str) -> str: + """Return the dashed path list under one named workflow trigger.""" + marker = f" {trigger}:\n paths:\n" + start = quality.index(marker) + len(marker) + lines: list[str] = [] + for line in quality[start:].splitlines(): + if line.startswith(" - "): + lines.append(line) + continue + if line.strip() == "": + continue + break + return "\n".join(lines) + + +def _compileall_block(quality: str) -> str: + """Return the compileall argument list from the focused quality job.""" + marker = "python -m compileall -q \\" + start = quality.index(marker) + remainder = quality[start:] + end = remainder.find("\n git ") + return remainder if end < 0 else remainder[:end] + + +def test_lineageweave_caller_is_hourly_bounded_and_non_cancelling() -> None: + """LineageWeave receives one realistic lineage repair without cancellation.""" + caller = _read(CALLER) + + assert 'cron: "4 * * * *"' in caller + assert "group: lineageweave-hourly-review-repair" in caller + assert "cancel-in-progress: false" in caller + assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller + assert "target_repository: ContextualWisdomLab/LineageWeave" in caller + assert "base_branch: main" in caller + assert 'max_prs: "50"' in caller + assert 'max_dispatches: "1"' in caller + assert 'retry_hours: "2"' in caller + + +def test_lineageweave_caller_preserves_oidc_and_explicit_secret_scope() -> None: + """The queue scanner maps established credentials without model secrets.""" + caller = _read(CALLER) + workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) + + assert "\npermissions:\n contents: read\n" in workflow_scope + assert ( + "\n permissions:\n contents: read\n id-token: write\n" + in jobs_scope + ) + assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller + assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller + assert "secrets: inherit" not in caller + assert "NVIDIA_NIM_API_KEY" not in caller + assert "COPILOT_GITHUB_TOKEN" not in caller + for forbidden in ( + "actions: write", + "contents: write", + "issues: write", + "pull-requests: write", + "statuses: write", + ): + assert forbidden not in caller + + +def test_lineageweave_target_is_not_hard_coded_in_shared_scheduler() -> None: + """Product identity remains in the thin caller rather than the engine.""" + assert "ContextualWisdomLab/LineageWeave" not in _read(SCHEDULER) + + +def test_lineageweave_doctoring_records_lineage_activation_and_credentials() -> None: + """Operators retain target-allowlist, lineage, and approval prerequisites.""" + doctoring = _read(DOCTORING) + + for phrase in ( + "ContextualWisdomLab/LineageWeave", + "OPENCODE_REPOSITORY_DISPATCH_TARGETS", + "independent non-author approval", + "NVIDIA_NIM_API_KEY", + "COPILOT_GITHUB_TOKEN", + "id-token: write", + "two-hour same-head retry floor", + "root-cause analysis", + "remediation feasibility", + "protected-main operational acceptance", + "APA 7th references", + "ContextualWisdomLab/LineageWeave#192", + "ContextualWisdomLab/LineageWeave#190", + "ContextualWisdomLab/LineageWeave#123", + "ContextualWisdomLab/LineageWeave#74", + ): + assert phrase in doctoring + + +def test_path_block_helpers_keep_trigger_and_compileall_sets_disjoint() -> None: + """A path listed only under push or compileall must not satisfy pull_request.""" + quality = ( + "on:\n" + " pull_request:\n" + " paths:\n" + " - .github/workflows/lineageweave-hourly-review-repair.yml\n" + " push:\n" + " paths:\n" + " - docs/doctoring/lineageweave-hourly-review-caller.md\n" + " python -m compileall -q \\\n" + " tests/test_lineageweave_hourly_review_caller.py\n" + " git diff --check\n" + ) + + pull_request_paths = _yaml_path_entries(_trigger_path_block(quality, "pull_request")) + push_paths = _yaml_path_entries(_trigger_path_block(quality, "push")) + compileall_paths = _yaml_path_entries(_compileall_block(quality)) + + assert pull_request_paths == {".github/workflows/lineageweave-hourly-review-repair.yml"} + assert push_paths == {"docs/doctoring/lineageweave-hourly-review-caller.md"} + assert compileall_paths == {"tests/test_lineageweave_hourly_review_caller.py"} + assert "docs/doctoring/lineageweave-hourly-review-caller.md" not in pull_request_paths + assert ".github/workflows/lineageweave-hourly-review-repair.yml" not in compileall_paths + + +def test_focused_quality_workflow_tracks_lineageweave_contracts() -> None: + """Caller, test, and doctoring edits always rerun the focused gate.""" + quality = _read(QUALITY_WORKFLOW) + pull_request_paths = _yaml_path_entries(_trigger_path_block(quality, "pull_request")) + push_paths = _yaml_path_entries(_trigger_path_block(quality, "push")) + compileall_paths = _yaml_path_entries(_compileall_block(quality)) + caller = ".github/workflows/lineageweave-hourly-review-repair.yml" + doctoring = "docs/doctoring/lineageweave-hourly-review-caller.md" + contract = "tests/test_lineageweave_hourly_review_caller.py" + + assert caller in pull_request_paths + assert doctoring in pull_request_paths + assert contract in pull_request_paths + assert caller in push_paths + assert doctoring in push_paths + assert contract in push_paths + assert contract in compileall_paths + assert caller not in compileall_paths + assert doctoring not in compileall_paths From 7baf4d23eec06bae1660d177d8216d8a9e0cbc1e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 23:48:47 +0900 Subject: [PATCH 02/20] fix: keep lineageweave quality scope minimal --- .../hourly-nvidia-nim-review-repair.yml | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/.github/workflows/hourly-nvidia-nim-review-repair.yml b/.github/workflows/hourly-nvidia-nim-review-repair.yml index c0781c37e..a9c6392f3 100644 --- a/.github/workflows/hourly-nvidia-nim-review-repair.yml +++ b/.github/workflows/hourly-nvidia-nim-review-repair.yml @@ -25,16 +25,7 @@ on: - tests/test_hourly_scheduler_runtime_budget.py - tests/test_nonnest2_hourly_review_caller.py - tests/test_originweave_hourly_review_caller.py - - .github/workflows/clearfolio-hourly-review-repair.yml - - .github/workflows/disksage-hourly-review-repair.yml - - .github/workflows/fast-mlsirm-hourly-review-repair.yml - - .github/workflows/hourly-nvidia-nim-review-repair.yml - .github/workflows/lineageweave-hourly-review-repair.yml - - scripts/ci/pr_review_conflict_scope.py - - scripts/ci/pr_review_autofix_context.py - - tests/test_disksage_hourly_review_caller.py - - tests/test_fast_mlsirm_hourly_review_caller.py - - tests/test_hourly_scheduler_runtime_budget.py - tests/test_lineageweave_hourly_review_caller.py - tests/test_hourly_autofix_context_quality_gate.py - tests/test_pr_review_conflict_scope.py @@ -59,7 +50,6 @@ on: - docs/doctoring/hourly-nvidia-nim-autofix.md - docs/doctoring/nonnest2-hourly-review-caller.md - docs/doctoring/originweave-hourly-review-caller.md - - docs/doctoring/hourly-nvidia-nim-autofix.md - docs/doctoring/lineageweave-hourly-review-caller.md push: paths: @@ -85,16 +75,7 @@ on: - tests/test_hourly_scheduler_runtime_budget.py - tests/test_nonnest2_hourly_review_caller.py - tests/test_originweave_hourly_review_caller.py - - .github/workflows/clearfolio-hourly-review-repair.yml - - .github/workflows/disksage-hourly-review-repair.yml - - .github/workflows/fast-mlsirm-hourly-review-repair.yml - - .github/workflows/hourly-nvidia-nim-review-repair.yml - .github/workflows/lineageweave-hourly-review-repair.yml - - scripts/ci/pr_review_conflict_scope.py - - scripts/ci/pr_review_autofix_context.py - - tests/test_disksage_hourly_review_caller.py - - tests/test_fast_mlsirm_hourly_review_caller.py - - tests/test_hourly_scheduler_runtime_budget.py - tests/test_lineageweave_hourly_review_caller.py - tests/test_hourly_autofix_context_quality_gate.py - tests/test_pr_review_conflict_scope.py @@ -119,7 +100,6 @@ on: - docs/doctoring/hourly-nvidia-nim-autofix.md - docs/doctoring/nonnest2-hourly-review-caller.md - docs/doctoring/originweave-hourly-review-caller.md - - docs/doctoring/hourly-nvidia-nim-autofix.md - docs/doctoring/lineageweave-hourly-review-caller.md permissions: @@ -176,9 +156,6 @@ jobs: tests/test_hourly_scheduler_runtime_budget.py \ tests/test_nonnest2_hourly_review_caller.py \ tests/test_originweave_hourly_review_caller.py \ - tests/test_disksage_hourly_review_caller.py \ - tests/test_fast_mlsirm_hourly_review_caller.py \ - tests/test_hourly_scheduler_runtime_budget.py \ tests/test_lineageweave_hourly_review_caller.py \ tests/test_pr_review_conflict_scope_control_files.py \ tests/test_hourly_autofix_context_quality_gate.py \ @@ -192,4 +169,3 @@ jobs: tests/test_pr_review_autofix_nvidia_nim_contract.py \ tests/test_pr_review_autofix_writer_security_contract.py git diff --check - git diff --check \ No newline at end of file From b2a9ea36bb8c72e266ac20baedd5af49561b64cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 16:33:00 -0700 Subject: [PATCH 03/20] test(automation): align LineageWeave incident evidence wording --- tests/test_lineageweave_hourly_review_caller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_lineageweave_hourly_review_caller.py b/tests/test_lineageweave_hourly_review_caller.py index 7e5b50e05..8da2f9542 100644 --- a/tests/test_lineageweave_hourly_review_caller.py +++ b/tests/test_lineageweave_hourly_review_caller.py @@ -97,7 +97,7 @@ def test_incident_doctoring_tracks_current_buyer_surface_stack() -> None: "#258 → #260 → #261 → #262 → #263 → #264", "@opencode-agent", "exact invocation ledger", - "without a visible receipt", + "neither a visible receipt", "formal OpenCode review", "no duplicate dispatch", "dependency order", From acf70d9fa4baf32bb83c09129be7192da13343a6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 17:07:14 -0700 Subject: [PATCH 04/20] test(automation): reject stale LineageWeave queue diagnosis --- tests/test_lineageweave_hourly_review_caller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_lineageweave_hourly_review_caller.py b/tests/test_lineageweave_hourly_review_caller.py index 8da2f9542..459980d30 100644 --- a/tests/test_lineageweave_hourly_review_caller.py +++ b/tests/test_lineageweave_hourly_review_caller.py @@ -83,9 +83,11 @@ def test_lineageweave_doctoring_preserves_operational_boundaries() -> None: "root-cause analysis", "remediation feasibility", "protected-main operational acceptance", + "`queue: max` is valid", "APA 7th references", ): assert phrase in doctoring + assert "unsupported concurrency key" not in doctoring def test_incident_doctoring_tracks_current_buyer_surface_stack() -> None: From 06933496d87f1d996c6f365df7e05590a4599414 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 17:08:33 -0700 Subject: [PATCH 05/20] docs(automation): correct LineageWeave concurrency incident record --- .../lineageweave-hourly-review-caller.md | 240 ++++++++++-------- 1 file changed, 141 insertions(+), 99 deletions(-) diff --git a/docs/doctoring/lineageweave-hourly-review-caller.md b/docs/doctoring/lineageweave-hourly-review-caller.md index 432e4b18c..350b69e35 100644 --- a/docs/doctoring/lineageweave-hourly-review-caller.md +++ b/docs/doctoring/lineageweave-hourly-review-caller.md @@ -1,141 +1,183 @@ # LineageWeave hourly review-repair caller -검토 기준일: **2026-08-17** +검토 기준일: **2026-08-20** ## Decision ContextualWisdomLab operates one protected hourly caller for -`ContextualWisdomLab/LineageWeave` (git-branch-style lineage DAGs from -scattered short records: multi-channel score fusion, LLM adjudication, -and Keyverse RP affiliation). The caller runs at minute 4, delegates to -the product-neutral central review-fix scheduler, inspects at most 50 -open pull requests targeting protected `main`, and dispatches at most -one bounded repair per heartbeat. - -A paying buyer of reconstructed affiliation lineage would feel live -LineageWeave pull requests stalling while hourly NVIDIA NIM repair -scanned only Clearfolio, DiskSage, and fast-mlsirm. Live heads such as -ContextualWisdomLab/LineageWeave#192 (plural affiliation next action), -ContextualWisdomLab/LineageWeave#190 (entity-resolution ADR), -ContextualWisdomLab/LineageWeave#123 (plural affiliation chips), and -ContextualWisdomLab/LineageWeave#74 (PROV-O actor lineage) target `main` -and never enter those other callers. - -The caller does not implement review or mutation logic itself. -LineageWeave remains standalone; Keyverse and naruon federate through -its RP profile without owning the lineage runtime. Privileged automation -stays in `ContextualWisdomLab/.github`. +`ContextualWisdomLab/LineageWeave`. The caller covers the current buyer-surface +stack **#258 → #260 → #261 → #262 → #263 → #264**, delegates to the +product-neutral central review-fix scheduler, inspects at most 50 open pull +requests targeting protected `main`, and dispatches at most one bounded repair +per heartbeat. + +The caller runs at minute 4 of every hour and also exposes `workflow_dispatch` +for an operator-controlled acceptance run. It does not contain product mutation +logic, LLM credentials, approval authority, merge authority, or release +authority. LineageWeave remains independently deployable; privileged automation +remains in `ContextualWisdomLab/.github`. + +## Buyer-visible incident + +A trusted `@opencode-agent` request on LineageWeave #258 did not produce a +visible receipt or a formal current-head OpenCode review. The durable repository +dispatch had already succeeded. The router then attempted a cosmetic `eyes` +reaction before publishing the acknowledgement. A target-repository HTTP 403 +therefore terminated the source run before the receipt was written. The exact +invocation ledger correctly prevented duplicate dispatch, but the former early +return also prevented a later sweep from healing the missing receipt. + +The scheduled sweep had a separate availability weakness: its `gh api` +subprocesses had no finite timeout, so generator shutdown could wait indefinitely +for an already-running repository request. The central repair now bounds every +request, preserves deterministic four-worker fanout, and allows a later sweep to +repair only the missing receipt without redispatch. ## Root-cause analysis and remediation feasibility The reusable worker performs exact-head root-cause analysis and tests -remediation feasibility before it edits. The reusable worker must: +remediation feasibility before it edits. It must: -1. Refetch the exact live head, base, reviews, checks, changed paths, and - writer state. -2. Establish the causal chain rather than repeat the terminal symptom. +1. Refetch the live head, declared base, stack dependency, formal reviews, + unresolved threads, checks, changed paths, and active writer state. +2. Establish the first causal boundary rather than repeat a terminal symptom. 3. Enumerate materially distinct minimal remedies. 4. Reject remedies that lack writer authority, cross sealed paths, require - unavailable credentials or protected-setting changes, violate stack - order, cannot be verified, or do not alter the diagnosed cause. -5. Dispatch at most one feasible repair. Otherwise leave the tree - unchanged. - -A queued or pending check remains a merge blocker but is not itself a -code finding. The independent non-author approval remains an external -authorization gate and is never synthesized by the repair worker. The -worker cannot approve, merge, release, resolve review findings by -inference, change protection, or manufacture passing checks. + unavailable credentials or protected-setting changes, violate stack order, + cannot be verified, or do not alter the diagnosed cause. +5. Dispatch at most one feasible repair; otherwise leave the branch unchanged. + +A queued or pending check remains a merge blocker but is not itself a code +finding. An independent non-author approval remains an external authorization +gate and is never synthesized by the repair worker. The worker cannot approve, +merge, release, weaken protection, dismiss valid findings by inference, or +manufacture passing evidence. + +## Stack order + +The active buyer surface is a dependency chain, not six unrelated pull +requests: + +```text +#258 + → #260 + → #261 + → #262 + → #263 + → #264 +``` + +A child is reviewed against its exact declared parent head. When a parent moves, +the child is stale until its base is updated and all exact-head checks and +reviews are regenerated. A green result from an ancestor, predecessor head, or +sibling cannot satisfy a descendant gate. ## Cadence and concurrency -The caller uses a single concurrency group and `cancel-in-progress: false`. -This preserves an in-flight bounded RCA instead of discarding lineage -evidence when the next hourly heartbeat arrives. The reusable scheduler -cancels only its own superseded short queue scan. - -The caller sets a **two-hour same-head retry floor**. Central OpenCode and -NVIDIA NIM work, plus entity-resolution or affiliation analysis, can -legitimately approach two hours. An hourly redispatch of the same -unchanged head would create duplicate writer pressure rather than faster -remediation. +The caller uses one repository-scoped concurrency group and +`cancel-in-progress: false`. A later heartbeat must not discard an in-flight +lineage, ontology, temporal-event, or buyer-surface root-cause analysis. -GitHub scheduled workflows can be delayed under load and execute only -from the default branch. The cron expression is a heartbeat, not a -real-time SLA. - -## Credential and model boundary +GitHub Actions **`queue: max` is valid** concurrency syntax as of May 7, 2026. +It preserves up to 100 pending runs when `cancel-in-progress` is false or +omitted. The central mention router therefore retains `queue: max`; the +LineageWeave heartbeat itself remains non-cancelling and bounded to one repair. -The caller keeps workflow `GITHUB_TOKEN` at `contents: read` and grants -the reusable job `id-token: write` so the central scheduler can mint the -OpenCode GitHub App token from GitHub OIDC when the mapped PAT is absent -(GitHub, n.d.-c). It maps only `PR_REVIEW_MERGE_TOKEN` and -`OPENCODE_APPROVE_TOKEN`. It never uses `secrets: inherit`, receives -`NVIDIA_NIM_API_KEY`, or introduces `COPILOT_GITHUB_TOKEN`. CWE-250 -forbids executing the caller with write or model privileges it does not -need (MITRE, 2026). +The caller sets a **two-hour same-head retry floor**. OpenCode and NVIDIA NIM +review, plus a full stacked-PR evidence pass, can legitimately exceed one hour. +Redispatching an unchanged head every hour would create duplicate writers rather +than faster remediation. -Model execution remains inside the central worker. The model credential -is the GitHub Secret `NVIDIA_NIM_API_KEY`; the caller does not receive or -forward it. +GitHub scheduled workflows execute from the default branch and can be delayed +under Actions load. The cron expression is a heartbeat, not a real-time SLA. +`workflow_dispatch` permits a deliberate post-merge acceptance run without +changing the cadence or credential boundary. -Before protected-main activation, the repository variable -`OPENCODE_REPOSITORY_DISPATCH_TARGETS` must contain the exact -`ContextualWisdomLab/LineageWeave` target. Missing or mismatched -configuration fails before mutation credential materialization. - -## Security, standalone operation, and modularity +## Credential and model boundary -The caller adds no LineageWeave runtime dependency, database object, -network endpoint, tenant authority, or product credential. LineageWeave -continues to run as a standalone lineage reconstructor. Keyverse, naruon, -and other CWL services may consume its affiliation evidence, but they -cannot weaken its exact-head, approval, or security gates. +The caller keeps workflow `GITHUB_TOKEN` at `contents: read`. Only the reusable +job receives `id-token: write`, allowing the reviewed central scheduler to use +its established GitHub OIDC credential path when required. The caller maps only +`PR_REVIEW_MERGE_TOKEN` and `OPENCODE_APPROVE_TOKEN`; it never uses +`secrets: inherit`, receives `NVIDIA_NIM_API_KEY`, or introduces +`COPILOT_GITHUB_TOKEN`. + +Model execution remains inside the separately reviewed central worker. Before +protected-main activation, `OPENCODE_REPOSITORY_DISPATCH_TARGETS` must contain +the exact `ContextualWisdomLab/LineageWeave` repository. A missing or mismatched +target fails before a mutation credential is materialized. + +## Operational acceptance + +Source checks are necessary but do not constitute **protected-main operational +acceptance**. Closure requires all of the following after the central repair and +this caller reach protected `main`: + +1. Post one fresh trusted `@opencode-agent` request against the then-current + exact head of LineageWeave #258. +2. Observe the central sibling-repository sweep discover that source comment. +3. Observe a durable exact-name invocation claim and a visible receipt containing + the source-comment marker and exact head. +4. Prove that a reaction failure cannot suppress the receipt. +5. Prove that the next sweep does not redispatch the same exact request. +6. Observe the downstream OpenCode workflow publish a formal exact-head review + or explicit fail-visible evidence. +7. Run the LineageWeave hourly caller and verify that it selects no more than one + eligible exact-head repair. +8. Re-evaluate #260 through #264 in dependency order after every parent-head + movement. + +Merge still requires zero unresolved valid findings, all required exact-head +checks, and qualifying independent approval. Static workflow syntax, a dispatch +receipt, or a green status alone is not a review verdict. ## Verification and rollback -Machine-checkable contracts require the exact target/base, minute 4 -cadence, non-cancelling single-flight group, one dispatch, two-hour -retry floor, explicit secret mapping, read-only contents plus job-scoped -`id-token: write`, focused path-filter coverage, and absence of model or -Copilot credentials. Independent `pull_request`, `push`, and `compileall` -path blocks must each name the caller, doctoring, or contract they own. +Machine-checkable contracts require: -After source integration, closure requires a scheduled or manual -protected-main consumer run proving the exact LineageWeave repository and -`main` base. Source checks alone are not protected-main operational acceptance. -Merge still requires zero unresolved valid findings and a -qualifying independent non-author approval. +- exact `ContextualWisdomLab/LineageWeave` target and protected `main` base; +- minute 4 hourly cadence plus a manual acceptance entry point; +- non-cancelling repository-scoped concurrency; +- at most one dispatch and a two-hour same-head retry floor; +- read-only workflow contents plus job-scoped OIDC; +- explicit scheduler-secret mapping; +- absence of model, Copilot, merge, release, and target-mutation authority; +- a focused path-filtered quality workflow for caller, test, and doctoring; and +- no product identity hard-coded in the reusable scheduler. -Rollback removes the LineageWeave caller, its focused test, doctoring, and -central path-filter/documentation entries. It must not remove scheduler -dispatch validation or affect independent product callers. +Rollback removes only this caller, its focused quality workflow, its contract +test, and its doctoring records. It must not remove the product-neutral +scheduler, the central mention-router repair, or another product caller. ## APA 7th references -GitHub, Inc. (n.d.-a). *Events that trigger workflows*. GitHub Docs. -Retrieved August 17, 2026, from +GitHub, Inc. (2026, May 7). *GitHub Actions concurrency groups now allow larger +queues*. GitHub Changelog. +https://github.blog/changelog/2026-05-07-github-actions-concurrency-groups-now-allow-larger-queues/ + +GitHub, Inc. (n.d.-a). *Events that trigger workflows*. GitHub Docs. Retrieved +August 20, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule -GitHub, Inc. (n.d.-b). *Reuse workflows*. GitHub Docs. Retrieved August -17, 2026, from +GitHub, Inc. (n.d.-b). *Reuse workflows*. GitHub Docs. Retrieved August 20, +2026, from https://docs.github.com/en/actions/how-tos/sharing-automations/reuse-workflows -GitHub, Inc. (n.d.-c). *Automatic token authentication*. GitHub Docs. -Retrieved August 17, 2026, from -https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication#permissions-for-the-github_token +GitHub, Inc. (n.d.-c). *OpenID Connect reference*. GitHub Docs. Retrieved +August 20, 2026, from +https://docs.github.com/en/actions/reference/security/oidc MITRE. (2026). *CWE-250: Execution with unnecessary privileges*. https://cwe.mitre.org/data/definitions/250.html National Institute of Standards and Technology. (2022). *Secure software -development framework (SSDF) version 1.1: Recommendations for mitigating -the risk of software vulnerabilities* (NIST Special Publication 800-218). +development framework (SSDF) version 1.1: Recommendations for mitigating the +risk of software vulnerabilities* (NIST Special Publication 800-218). https://doi.org/10.6028/NIST.SP.800-218 -NVIDIA. (n.d.). *NVIDIA NIM for large language models documentation*. -Retrieved August 17, 2026, from +NVIDIA. (n.d.). *NVIDIA NIM for large language models documentation*. Retrieved +August 20, 2026, from https://docs.nvidia.com/nim/large-language-models/latest/ -OpenCode. (n.d.). *OpenCode documentation*. Retrieved August 17, 2026, -from https://opencode.ai/docs/ +OpenCode. (n.d.). *OpenCode documentation*. Retrieved August 20, 2026, from +https://opencode.ai/docs/ From a633d2af97adf148f1df9f31812d1143bbaa583d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 17:11:07 -0700 Subject: [PATCH 06/20] docs(automation): keep operational acceptance contract literal --- docs/doctoring/lineageweave-hourly-review-caller.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/lineageweave-hourly-review-caller.md b/docs/doctoring/lineageweave-hourly-review-caller.md index 350b69e35..a0dadc34f 100644 --- a/docs/doctoring/lineageweave-hourly-review-caller.md +++ b/docs/doctoring/lineageweave-hourly-review-caller.md @@ -109,9 +109,9 @@ target fails before a mutation credential is materialized. ## Operational acceptance -Source checks are necessary but do not constitute **protected-main operational -acceptance**. Closure requires all of the following after the central repair and -this caller reach protected `main`: +Source checks are necessary but do not constitute protected-main operational acceptance. +Closure requires all of the following after the central repair and this caller +reach protected `main`: 1. Post one fresh trusted `@opencode-agent` request against the then-current exact head of LineageWeave #258. From 86fa5f4dd8e286ba9cc838aa89fd61e879b39cea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 17:44:32 -0700 Subject: [PATCH 07/20] test(automation): reproduce LineageWeave descendant exclusion --- .../one-shot-repair-lineageweave-stack.yml | 626 ++++++++++++++++++ 1 file changed, 626 insertions(+) create mode 100644 .github/workflows/one-shot-repair-lineageweave-stack.yml diff --git a/.github/workflows/one-shot-repair-lineageweave-stack.yml b/.github/workflows/one-shot-repair-lineageweave-stack.yml new file mode 100644 index 000000000..d9cfc4304 --- /dev/null +++ b/.github/workflows/one-shot-repair-lineageweave-stack.yml @@ -0,0 +1,626 @@ +name: One-shot repair LineageWeave stack scheduler + +on: + push: + branches: + - feat/lineageweave-hourly-nvidia-nim-review-repair + paths: + - .github/workflows/one-shot-repair-lineageweave-stack.yml + +permissions: + contents: write + +concurrency: + group: one-shot-repair-lineageweave-stack + cancel-in-progress: false + +jobs: + repair: + runs-on: ubuntu-24.04 + timeout-minutes: 20 + steps: + - name: Checkout exact repair trigger + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ github.sha }} + fetch-depth: 2 + - name: Verify branch and parent + env: + EXPECTED_BRANCH: feat/lineageweave-hourly-nvidia-nim-review-repair + EXPECTED_PARENT: a633d2af97adf148f1df9f31812d1143bbaa583d + run: | + set -euo pipefail + test "$GITHUB_REF_NAME" = "$EXPECTED_BRANCH" + test "$(git rev-parse HEAD^)" = "$EXPECTED_PARENT" + - name: Apply ordered-stack RED to GREEN repair + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + + def replace_once(path: str, old: str, new: str) -> None: + target = Path(path) + text = target.read_text(encoding="utf-8") + if text.count(old) != 1: + raise SystemExit(f"expected exactly one match in {path}: {old!r}") + target.write_text(text.replace(old, new), encoding="utf-8") + + scheduler = ".github/workflows/pr-review-fix-scheduler.yml" + replace_once( + scheduler, + ''' base_branch: + description: Base branch to scan; defaults to the caller repository default branch + required: false + default: "" + type: string + canonical_ref:''', + ''' base_branch: + description: Base branch to scan; defaults to the caller repository default branch + required: false + default: "" + type: string + pull_request_numbers: + description: Optional comma-separated PR numbers inspected in the supplied dependency order + required: false + default: "" + type: string + canonical_ref:''', + ) + expression = "$" + "{{ github.event.client_payload.pull_request_numbers || inputs.pull_request_numbers || '' }}" + replace_once( + scheduler, + ' DEFAULT_BRANCH: ${{ github.event.client_payload.base_branch || inputs.base_branch || vars.PR_REVIEW_FIX_BASE_BRANCH || github.event.repository.default_branch }}\n DRY_RUN:', + ' DEFAULT_BRANCH: ${{ github.event.client_payload.base_branch || inputs.base_branch || vars.PR_REVIEW_FIX_BASE_BRANCH || github.event.repository.default_branch }}\n' + f' PULL_REQUEST_NUMBERS: {expression}\n' + ' DRY_RUN:', + ) + replace_once( + scheduler, + ' - name: Self-test fix scheduler contract\n run: python3 scripts/ci/pr_review_fix_scheduler.py --self-test', + ' - name: Self-test fix scheduler contracts\n run: |\n python3 scripts/ci/pr_review_fix_scheduler.py --self-test\n python3 scripts/ci/pr_review_fix_stack_scheduler.py --self-test', + ) + replace_once( + scheduler, + ''' if [ "$RESOLVE_UNREVIEWED_CONFLICTS" = "true" ]; then + args+=(--resolve-unreviewed-conflicts) + fi + if [ "$DRY_RUN" = "true" ]; then + args+=(--dry-run) + fi + python3 scripts/ci/pr_review_fix_scheduler.py "${args[@]}"''', + ''' scheduler_script="scripts/ci/pr_review_fix_scheduler.py" + if [ -n "$PULL_REQUEST_NUMBERS" ]; then + scheduler_script="scripts/ci/pr_review_fix_stack_scheduler.py" + args+=(--pull-request-numbers "$PULL_REQUEST_NUMBERS") + fi + if [ "$RESOLVE_UNREVIEWED_CONFLICTS" = "true" ]; then + args+=(--resolve-unreviewed-conflicts) + fi + if [ "$DRY_RUN" = "true" ]; then + args+=(--dry-run) + fi + python3 "$scheduler_script" "${args[@]}"''', + ) + + Path("scripts/ci/pr_review_fix_stack_scheduler.py").write_text(r'''#!/usr/bin/env python3 + """Dispatch at most one repair across an explicitly ordered pull-request stack.""" + + from __future__ import annotations + + import argparse + import json + import os + import re + from typing import Any + + try: + from pr_review_fix_scheduler import ( + DEFAULT_AUTOFIX_REPOSITORY, + DEFAULT_AUTOFIX_WORKFLOW, + REPO_RE, + fetch_pr, + inspect_pr, + ) + except ModuleNotFoundError: + from scripts.ci.pr_review_fix_scheduler import ( + DEFAULT_AUTOFIX_REPOSITORY, + DEFAULT_AUTOFIX_WORKFLOW, + REPO_RE, + fetch_pr, + inspect_pr, + ) + + BRANCH_RE = re.compile(r"^(?!-)[A-Za-z0-9._/-]+$") + SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") + NUMBER_RE = re.compile(r"^[1-9][0-9]*$") + NO_REPAIR_REASON = ( + "no current-head autofixable review, failed-check RCA, or approved merge conflict" + ) + + + def parse_pull_request_numbers(raw: str, *, maximum: int) -> tuple[int, ...]: + """Return unique positive PR numbers while preserving caller order.""" + + if maximum < 1: + raise ValueError("maximum must be positive") + tokens = tuple(part.strip() for part in raw.split(",") if part.strip()) + if not tokens: + raise ValueError("at least one pull request number is required") + numbers: list[int] = [] + seen: set[int] = set() + for token in tokens: + if not NUMBER_RE.fullmatch(token): + raise ValueError(f"invalid pull request number: {token!r}") + number = int(token) + if number in seen: + raise ValueError(f"duplicate pull request number: {number}") + seen.add(number) + numbers.append(number) + if len(numbers) > maximum: + raise ValueError( + f"pull request stack has {len(numbers)} entries; maximum is {maximum}" + ) + return tuple(numbers) + + + def _single_pull_request(repo: str, number: int) -> dict[str, Any]: + """Fetch exactly one live pull request or fail closed.""" + + records = fetch_pr(repo, number) + if len(records) != 1: + raise RuntimeError( + f"expected one live pull request for #{number}; received {len(records)}" + ) + return records[0] + + + def _validate_stack_edge( + pr: dict[str, Any], + *, + expected_base_name: str, + expected_base_oid: str | None, + ) -> None: + """Require the current child base to equal the declared parent branch and head.""" + + actual_name = str(pr.get("baseRefName") or "") + actual_oid = str(pr.get("baseRefOid") or "") + if actual_name != expected_base_name: + raise RuntimeError( + f"PR #{pr.get('number')} base branch is {actual_name!r}; " + f"expected {expected_base_name!r}" + ) + if expected_base_oid is not None and actual_oid.lower() != expected_base_oid.lower(): + raise RuntimeError( + f"PR #{pr.get('number')} base SHA is {actual_oid or ''}; " + f"expected parent head {expected_base_oid}" + ) + + + def process_stack(args: argparse.Namespace) -> int: + """Inspect the stack in order and stop after one dispatch or blocker.""" + + previous: dict[str, Any] | None = None + inspected = 0 + dispatched = 0 + failed = False + decisions: list[dict[str, Any]] = [] + for number in args.pull_request_numbers: + if dispatched: + break + try: + pr = _single_pull_request(args.repo, number) + expected_name = ( + args.base_branch + if previous is None + else str(previous.get("headRefName") or "") + ) + expected_oid = ( + None + if previous is None + else str(previous.get("headRefOid") or "") + ) + _validate_stack_edge( + pr, + expected_base_name=expected_name, + expected_base_oid=expected_oid, + ) + local_args = argparse.Namespace(**vars(args)) + local_args.base_branch = expected_name + action, reasons = inspect_pr(args.repo, pr, local_args) + except (RuntimeError, ValueError) as exc: + action, reasons = "error", (str(exc),) + inspected += 1 + decisions.append( + {"pr": number, "action": action, "reasons": list(reasons)} + ) + print(f"PR #{number}: {action}: {'; '.join(reasons)}") + if action == "dispatch": + dispatched = 1 + break + if action == "error": + failed = True + break + if action == "wait": + break + if action != "skip" or tuple(reasons) != (NO_REPAIR_REASON,): + break + previous = pr + print( + json.dumps( + { + "inspected": inspected, + "autofix_dispatches": dispatched, + "stack_prs": list(args.pull_request_numbers), + "decisions": decisions, + }, + sort_keys=True, + ) + ) + return 1 if failed else 0 + + + def self_test() -> int: + """Exercise parser invariants without network or repository mutation.""" + + assert parse_pull_request_numbers("258,260,261", maximum=3) == ( + 258, + 260, + 261, + ) + try: + parse_pull_request_numbers("258,258", maximum=3) + except ValueError: + pass + else: # pragma: no cover - defensive contract + raise AssertionError("duplicate PR numbers must fail") + print("stack self-test passed") + return 0 + + + def parse_args(argv: list[str]) -> argparse.Namespace: + """Parse the ordered-stack scheduler CLI contract.""" + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo", default=os.environ.get("GITHUB_REPOSITORY", "")) + parser.add_argument("--base-branch", default=os.environ.get("DEFAULT_BRANCH", "")) + parser.add_argument( + "--pull-request-numbers", + default=os.environ.get("PULL_REQUEST_NUMBERS", ""), + ) + parser.add_argument("--max-prs", type=int, default=50) + parser.add_argument("--max-dispatches", type=int, default=1) + parser.add_argument("--retry-hours", type=int, default=24) + parser.add_argument("--resolve-unreviewed-conflicts", action="store_true") + parser.add_argument("--autofix-workflow", default=DEFAULT_AUTOFIX_WORKFLOW) + parser.add_argument("--autofix-repository", default=DEFAULT_AUTOFIX_REPOSITORY) + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--self-test", action="store_true") + args = parser.parse_args(argv) + if args.self_test: + return args + if not REPO_RE.fullmatch(args.repo): + parser.error("--repo must be in OWNER/NAME form") + if not BRANCH_RE.fullmatch(args.base_branch): + parser.error("--base-branch is required and must be a safe branch name") + if args.max_prs < 1: + parser.error("--max-prs must be positive") + if args.max_dispatches != 1: + parser.error("ordered stack scheduling requires --max-dispatches 1") + if args.retry_hours < 1: + parser.error("--retry-hours must be positive") + try: + args.pull_request_numbers = parse_pull_request_numbers( + args.pull_request_numbers, + maximum=args.max_prs, + ) + except ValueError as exc: + parser.error(str(exc)) + return args + + + def main(argv: list[str] | None = None) -> int: + """Run the self-test or process one ordered stack.""" + + args = parse_args(list(argv or [])) + if args.self_test: + return self_test() + return process_stack(args) + + + if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) + ''').replace(" ", ""), encoding="utf-8") + + Path("tests/test_pr_review_fix_stack_scheduler.py").write_text(r'''"""Tests for exact ordered-stack review repair selection.""" + + from __future__ import annotations + + import argparse + import json + + import pytest + + from scripts.ci import pr_review_fix_stack_scheduler as stack + + + def make_pr( + number: int, + *, + base_name: str, + base_oid: str, + head_name: str, + head_oid: str, + ) -> dict: + """Return one minimal scheduler-shaped pull request.""" + + return { + "number": number, + "baseRefName": base_name, + "baseRefOid": base_oid, + "headRefName": head_name, + "headRefOid": head_oid, + } + + + def arguments(numbers: tuple[int, ...]) -> argparse.Namespace: + """Return the runtime arguments consumed by the stack driver.""" + + return argparse.Namespace( + repo="ContextualWisdomLab/LineageWeave", + base_branch="main", + pull_request_numbers=numbers, + max_prs=6, + max_dispatches=1, + retry_hours=2, + resolve_unreviewed_conflicts=True, + autofix_workflow="pr-review-autofix.yml", + autofix_repository="ContextualWisdomLab/.github", + dry_run=False, + ) + + + def test_parse_pull_request_numbers_preserves_order_and_rejects_ambiguity() -> None: + """The explicit queue is positive, unique, bounded, and ordered.""" + + assert stack.parse_pull_request_numbers("258, 260,261", maximum=3) == ( + 258, + 260, + 261, + ) + for raw, maximum in (("", 3), ("0", 3), ("x", 3), ("258,258", 3), ("1,2", 1)): + with pytest.raises(ValueError): + stack.parse_pull_request_numbers(raw, maximum=maximum) + with pytest.raises(ValueError): + stack.parse_pull_request_numbers("1", maximum=0) + + + def test_stack_dispatches_once_in_declared_dependency_order( + monkeypatch, + capsys, + ) -> None: + """A clean parent advances to the first actionable child and then stops.""" + + root = make_pr( + 258, + base_name="main", + base_oid="0" * 40, + head_name="feat/root", + head_oid="1" * 40, + ) + child = make_pr( + 260, + base_name="feat/root", + base_oid="1" * 40, + head_name="feat/child", + head_oid="2" * 40, + ) + grandchild = make_pr( + 261, + base_name="feat/child", + base_oid="2" * 40, + head_name="feat/grandchild", + head_oid="3" * 40, + ) + records = {258: root, 260: child, 261: grandchild} + fetched: list[int] = [] + inspected: list[tuple[int, str]] = [] + + def fake_fetch(repo: str, number: int) -> list[dict]: + assert repo == "ContextualWisdomLab/LineageWeave" + fetched.append(number) + return [records[number]] + + def fake_inspect(repo: str, pr: dict, args: argparse.Namespace): + assert repo == "ContextualWisdomLab/LineageWeave" + inspected.append((pr["number"], args.base_branch)) + if pr["number"] == 258: + return "skip", (stack.NO_REPAIR_REASON,) + return "dispatch", ("current-head OpenCode requested changes",) + + monkeypatch.setattr(stack, "fetch_pr", fake_fetch) + monkeypatch.setattr(stack, "inspect_pr", fake_inspect) + + assert stack.process_stack(arguments((258, 260, 261))) == 0 + assert fetched == [258, 260] + assert inspected == [(258, "main"), (260, "feat/root")] + payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) + assert payload["autofix_dispatches"] == 1 + assert [decision["pr"] for decision in payload["decisions"]] == [258, 260] + + + def test_stack_fails_closed_when_child_is_not_on_exact_parent_head( + monkeypatch, + capsys, + ) -> None: + """A stale descendant cannot be repaired using ancestor evidence.""" + + root = make_pr( + 258, + base_name="main", + base_oid="0" * 40, + head_name="feat/root", + head_oid="1" * 40, + ) + stale_child = make_pr( + 260, + base_name="feat/root", + base_oid="9" * 40, + head_name="feat/child", + head_oid="2" * 40, + ) + records = {258: root, 260: stale_child} + inspected: list[int] = [] + monkeypatch.setattr(stack, "fetch_pr", lambda repo, number: [records[number]]) + + def fake_inspect(repo: str, pr: dict, args: argparse.Namespace): + inspected.append(pr["number"]) + return "skip", (stack.NO_REPAIR_REASON,) + + monkeypatch.setattr(stack, "inspect_pr", fake_inspect) + + assert stack.process_stack(arguments((258, 260))) == 1 + assert inspected == [258] + payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) + assert payload["decisions"][-1]["action"] == "error" + assert "expected parent head" in payload["decisions"][-1]["reasons"][0] + + + def test_stack_stops_on_wait_and_nonrepair_skip(monkeypatch) -> None: + """An in-flight or structurally blocked parent prevents child mutation.""" + + pr = make_pr( + 258, + base_name="main", + base_oid="0" * 40, + head_name="feat/root", + head_oid="1" * 40, + ) + monkeypatch.setattr(stack, "fetch_pr", lambda repo, number: [pr]) + monkeypatch.setattr( + stack, + "inspect_pr", + lambda repo, item, args: ("wait", ("recent autofix marker exists",)), + ) + assert stack.process_stack(arguments((258, 260))) == 0 + + monkeypatch.setattr( + stack, + "inspect_pr", + lambda repo, item, args: ("skip", ("draft PR",)), + ) + assert stack.process_stack(arguments((258, 260))) == 0 + + + def test_stack_handles_missing_pr_and_cli_contract(monkeypatch) -> None: + """Missing records and unsafe CLI values fail before descendant mutation.""" + + monkeypatch.setattr(stack, "fetch_pr", lambda repo, number: []) + assert stack.process_stack(arguments((258,))) == 1 + assert stack.main(["--self-test"]) == 0 + parsed = stack.parse_args( + [ + "--repo", + "ContextualWisdomLab/LineageWeave", + "--base-branch", + "main", + "--pull-request-numbers", + "258,260", + "--max-prs", + "2", + "--max-dispatches", + "1", + ] + ) + assert parsed.pull_request_numbers == (258, 260) + for bad in ( + ["--repo", "bad repo", "--base-branch", "main", "--pull-request-numbers", "258"], + ["--repo", "owner/repo", "--base-branch", "-bad", "--pull-request-numbers", "258"], + ["--repo", "owner/repo", "--base-branch", "main", "--pull-request-numbers", "258", "--max-prs", "0"], + ["--repo", "owner/repo", "--base-branch", "main", "--pull-request-numbers", "258", "--max-dispatches", "2"], + ["--repo", "owner/repo", "--base-branch", "main", "--pull-request-numbers", "258", "--retry-hours", "0"], + ): + with pytest.raises(SystemExit): + stack.parse_args(bad) + ''').replace(" ", ""), encoding="utf-8") + + caller = ".github/workflows/lineageweave-hourly-review-repair.yml" + replace_once( + caller, + ' base_branch: main\n max_prs: "50"', + ' base_branch: main\n pull_request_numbers: "258,260,261,262,263,264"\n max_prs: "6"', + ) + + caller_test = "tests/test_lineageweave_hourly_review_caller.py" + replace_once( + caller_test, + ' assert "base_branch: main" in caller\n assert \'max_prs: "50"\' in caller', + ' assert "base_branch: main" in caller\n assert \'pull_request_numbers: "258,260,261,262,263,264"\' in caller\n assert \'max_prs: "6"\' in caller', + ) + replace_once( + caller_test, + ' ".github/workflows/lineageweave-hourly-review-repair-quality.yml",\n "tests/test_lineageweave_hourly_review_caller.py",', + ' ".github/workflows/lineageweave-hourly-review-repair-quality.yml",\n ".github/workflows/pr-review-fix-scheduler.yml",\n "scripts/ci/pr_review_fix_stack_scheduler.py",\n "tests/test_pr_review_fix_stack_scheduler.py",\n "tests/test_lineageweave_hourly_review_caller.py",', + ) + + quality = ".github/workflows/lineageweave-hourly-review-repair-quality.yml" + for section in ("pull_request", "push"): + marker = f" {section}:\n paths:\n" + text = Path(quality).read_text(encoding="utf-8") + if text.count(marker) != 1: + raise SystemExit(f"missing {section} path marker") + addition = ( + marker + + " - .github/workflows/pr-review-fix-scheduler.yml\n" + + " - scripts/ci/pr_review_fix_stack_scheduler.py\n" + + " - tests/test_pr_review_fix_stack_scheduler.py\n" + ) + Path(quality).write_text(text.replace(marker, addition), encoding="utf-8") + replace_once( + quality, + ' python -m pytest -q tests/test_lineageweave_hourly_review_caller.py\n python -m compileall -q \\\n tests/test_lineageweave_hourly_review_caller.py', + ' python -m pytest -q \\\n tests/test_lineageweave_hourly_review_caller.py \\\n tests/test_pr_review_fix_stack_scheduler.py \\\n tests/test_pr_review_fix_scheduler.py\n python -m compileall -q \\\n scripts/ci/pr_review_fix_stack_scheduler.py \\\n tests/test_lineageweave_hourly_review_caller.py \\\n tests/test_pr_review_fix_stack_scheduler.py', + ) + + doctor = Path("docs/doctoring/lineageweave-hourly-review-caller.md") + text = doctor.read_text(encoding="utf-8") + anchor = "The caller runs at minute 4 of every hour and also exposes `workflow_dispatch`\n" + paragraph = ( + "The caller supplies the explicit ordered queue `258,260,261,262,263,264`. " + "The shared scheduler validates every child base branch and base SHA against " + "the immediately preceding parent head, continues only when the parent has no " + "repairable exact-head evidence, and stops after the first dispatch, wait, or " + "structural blocker.\n\n" + ) + if text.count(anchor) != 1: + raise SystemExit("doctoring insertion anchor missing") + doctor.write_text(text.replace(anchor, paragraph + anchor), encoding="utf-8") + PY + - name: Run focused regression suite + run: | + set -euo pipefail + python -m pytest -q \ + tests/test_lineageweave_hourly_review_caller.py \ + tests/test_pr_review_fix_stack_scheduler.py \ + tests/test_pr_review_fix_scheduler.py + python -m compileall -q \ + scripts/ci/pr_review_fix_stack_scheduler.py \ + tests/test_lineageweave_hourly_review_caller.py \ + tests/test_pr_review_fix_stack_scheduler.py + git diff --check + - name: Commit verified repair and remove one-shot workflow + run: | + set -euo pipefail + rm .github/workflows/one-shot-repair-lineageweave-stack.yml + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add \ + .github/workflows/pr-review-fix-scheduler.yml \ + .github/workflows/lineageweave-hourly-review-repair.yml \ + .github/workflows/lineageweave-hourly-review-repair-quality.yml \ + scripts/ci/pr_review_fix_stack_scheduler.py \ + tests/test_pr_review_fix_stack_scheduler.py \ + tests/test_lineageweave_hourly_review_caller.py \ + docs/doctoring/lineageweave-hourly-review-caller.md \ + .github/workflows/one-shot-repair-lineageweave-stack.yml + git commit -m "fix(automation): traverse LineageWeave stack in dependency order" + git push origin HEAD:feat/lineageweave-hourly-nvidia-nim-review-repair From 18d7a945b462f3c189d5d1bac9e933e07557e517 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 20:04:22 -0700 Subject: [PATCH 08/20] fix(automation): repair LineageWeave stack in dependency order --- ...eageweave-hourly-review-repair-quality.yml | 25 +- .../lineageweave-hourly-review-repair.yml | 167 ++++- .../one-shot-repair-lineageweave-stack.yml | 626 ------------------ ...geweave-buyer-surface-opencode-incident.md | 10 +- .../lineageweave-hourly-review-caller.md | 63 +- scripts/ci/pr_review_fix_stack_scheduler.py | 264 ++++++++ .../test_lineageweave_hourly_review_caller.py | 66 +- tests/test_pr_review_fix_stack_scheduler.py | 371 +++++++++++ 8 files changed, 900 insertions(+), 692 deletions(-) delete mode 100644 .github/workflows/one-shot-repair-lineageweave-stack.yml create mode 100644 scripts/ci/pr_review_fix_stack_scheduler.py create mode 100644 tests/test_pr_review_fix_stack_scheduler.py diff --git a/.github/workflows/lineageweave-hourly-review-repair-quality.yml b/.github/workflows/lineageweave-hourly-review-repair-quality.yml index c2140f116..6f8fa4f7b 100644 --- a/.github/workflows/lineageweave-hourly-review-repair-quality.yml +++ b/.github/workflows/lineageweave-hourly-review-repair-quality.yml @@ -5,6 +5,8 @@ on: paths: - .github/workflows/lineageweave-hourly-review-repair.yml - .github/workflows/lineageweave-hourly-review-repair-quality.yml + - scripts/ci/pr_review_fix_stack_scheduler.py + - tests/test_pr_review_fix_stack_scheduler.py - tests/test_lineageweave_hourly_review_caller.py - docs/doctoring/lineageweave-hourly-review-caller.md - docs/doctoring/lineageweave-buyer-surface-opencode-incident.md @@ -12,6 +14,8 @@ on: paths: - .github/workflows/lineageweave-hourly-review-repair.yml - .github/workflows/lineageweave-hourly-review-repair-quality.yml + - scripts/ci/pr_review_fix_stack_scheduler.py + - tests/test_pr_review_fix_stack_scheduler.py - tests/test_lineageweave_hourly_review_caller.py - docs/doctoring/lineageweave-hourly-review-caller.md - docs/doctoring/lineageweave-buyer-surface-opencode-incident.md @@ -25,7 +29,7 @@ concurrency: jobs: contract: - name: LineageWeave target, cadence, and least-privilege contract + name: LineageWeave ordered-stack and least-privilege contract runs-on: ubuntu-24.04 timeout-minutes: 10 steps: @@ -34,7 +38,7 @@ jobs: with: egress-policy: audit - name: Checkout exact source revision - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} persist-credentials: false @@ -46,10 +50,21 @@ jobs: run: >- python -m pip install --disable-pip-version-check --require-hashes -r requirements-opencode-review-ci-hashes.txt - - name: Verify LineageWeave hourly caller contracts + - name: Verify ordered-stack repair contracts run: | set -euo pipefail - python -m pytest -q tests/test_lineageweave_hourly_review_caller.py + python -m coverage erase + python -m coverage run --branch -m pytest -q \ + tests/test_lineageweave_hourly_review_caller.py \ + tests/test_pr_review_fix_stack_scheduler.py \ + tests/test_pr_review_fix_scheduler.py + python -m coverage report \ + --include=scripts/ci/pr_review_fix_stack_scheduler.py \ + --fail-under=100 + interrogate -vv --fail-under 100 \ + scripts/ci/pr_review_fix_stack_scheduler.py python -m compileall -q \ - tests/test_lineageweave_hourly_review_caller.py + scripts/ci/pr_review_fix_stack_scheduler.py \ + tests/test_lineageweave_hourly_review_caller.py \ + tests/test_pr_review_fix_stack_scheduler.py git diff --check diff --git a/.github/workflows/lineageweave-hourly-review-repair.yml b/.github/workflows/lineageweave-hourly-review-repair.yml index 69de22bd4..05746ff1b 100644 --- a/.github/workflows/lineageweave-hourly-review-repair.yml +++ b/.github/workflows/lineageweave-hourly-review-repair.yml @@ -2,6 +2,12 @@ name: LineageWeave Hourly Review Repair on: workflow_dispatch: + inputs: + dry_run: + description: Inspect the ordered stack without dispatching a repair + required: false + default: false + type: boolean schedule: # Minute 4 avoids the start-of-hour load peak and existing product callers. - cron: "4 * * * *" @@ -16,16 +22,157 @@ permissions: jobs: dispatch-review-repair: + runs-on: ubuntu-24.04 + timeout-minutes: 35 permissions: contents: read id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/LineageWeave - base_branch: main - max_prs: "50" - max_dispatches: "1" - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} + env: + TARGET_REPOSITORY: ContextualWisdomLab/LineageWeave + ROOT_BASE_BRANCH: main + PULL_REQUEST_NUMBERS: "258,260,261,262,263,264" + MAX_PRS: "6" + MAX_DISPATCHES: "1" + RETRY_HOURS: "2" + AUTOFIX_WORKFLOW: pr-review-autofix.yml + AUTOFIX_REPOSITORY: ContextualWisdomLab/.github + DRY_RUN: ${{ inputs.dry_run == true }} + steps: + - name: Harden runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Validate protected source and target authority + env: + ALLOWED_TARGET_REPOSITORIES: ${{ vars.OPENCODE_REPOSITORY_DISPATCH_TARGETS }} + run: | + set -euo pipefail + if [ "$GITHUB_REPOSITORY" != "ContextualWisdomLab/.github" ] || + [ "$GITHUB_REF" != "refs/heads/main" ]; then + echo "::error::LineageWeave repair may run only from protected ContextualWisdomLab/.github main." + exit 1 + fi + + target_allowed=false + IFS=',' read -r -a allowed_targets <<<"${ALLOWED_TARGET_REPOSITORIES:-}" + for candidate in "${allowed_targets[@]}"; do + candidate="${candidate//[[:space:]]/}" + if [ "$candidate" = "$TARGET_REPOSITORY" ]; then + target_allowed=true + break + fi + done + if [ "$target_allowed" != "true" ]; then + echo "::error::ContextualWisdomLab/LineageWeave is absent from OPENCODE_REPOSITORY_DISPATCH_TARGETS." + exit 1 + fi + + - name: Exchange OpenCode app token for scheduler mutations + id: scheduler_app_token + env: + OIDC_AUDIENCE: opencode-github-action + OPENCODE_API_BASE_URL: https://api.opencode.ai + USER_TOKEN_CONFIGURED: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '' }} + run: | + set -euo pipefail + mark_unavailable() { + echo "available=false" >>"$GITHUB_OUTPUT" + } + + if [ "$USER_TOKEN_CONFIGURED" = "true" ]; then + echo "A configured cross-repository user token takes precedence." + mark_unavailable + exit 0 + fi + if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || + [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then + echo "OpenCode app token exchange unavailable: OIDC request environment is missing." + mark_unavailable + exit 0 + fi + + request_url="${ACTIONS_ID_TOKEN_REQUEST_URL}" + separator="&" + case "$request_url" in + *\?*) ;; + *) separator="?" ;; + esac + if ! oidc_response="$( + curl -fsS --connect-timeout 10 --max-time 30 \ + -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ + "${request_url}${separator}audience=${OIDC_AUDIENCE}" + )"; then + echo "OpenCode app token exchange unavailable: OIDC token request did not complete." + mark_unavailable + exit 0 + fi + oidc_token="$(jq -r '.value // empty' <<<"$oidc_response")" + if [ -z "$oidc_token" ]; then + echo "OpenCode app token exchange unavailable: OIDC token response was empty." + mark_unavailable + exit 0 + fi + if ! token_response="$( + curl -fsS --connect-timeout 10 --max-time 30 -X POST \ + -H "Authorization: Bearer ${oidc_token}" \ + "${OPENCODE_API_BASE_URL}/exchange_github_app_token" + )"; then + echo "OpenCode app token exchange unavailable: app token request did not complete." + mark_unavailable + exit 0 + fi + app_token="$(jq -r '.token // empty' <<<"$token_response")" + if [ -z "$app_token" ]; then + echo "OpenCode app token exchange unavailable: app token response was empty." + mark_unavailable + exit 0 + fi + + echo "::add-mask::$app_token" + { + echo "available=true" + echo "token=$app_token" + } >>"$GITHUB_OUTPUT" + + - name: Checkout exact protected source revision + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ github.sha }} + fetch-depth: 1 + persist-credentials: false + + - name: Verify exact source and scheduler contracts + run: | + set -euo pipefail + test "$(git rev-parse HEAD)" = "$GITHUB_SHA" + python3 scripts/ci/pr_review_fix_scheduler.py --self-test + python3 scripts/ci/pr_review_fix_stack_scheduler.py --self-test + git diff --check + + - name: Dispatch one dependency-safe review repair + env: + GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token }} + MUTATION_CREDENTIAL_AVAILABLE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '' || steps.scheduler_app_token.outputs.available == 'true' }} + run: | + set -euo pipefail + if [ "$MUTATION_CREDENTIAL_AVAILABLE" != "true" ]; then + echo "::error::An established scheduler mutation credential or exchanged OpenCode app token is required." + exit 1 + fi + + args=( + --repo "$TARGET_REPOSITORY" + --base-branch "$ROOT_BASE_BRANCH" + --pull-request-numbers "$PULL_REQUEST_NUMBERS" + --max-prs "$MAX_PRS" + --max-dispatches "$MAX_DISPATCHES" + --retry-hours "$RETRY_HOURS" + --resolve-unreviewed-conflicts + --autofix-workflow "$AUTOFIX_WORKFLOW" + --autofix-repository "$AUTOFIX_REPOSITORY" + ) + if [ "$DRY_RUN" = "true" ]; then + args+=(--dry-run) + fi + python3 scripts/ci/pr_review_fix_stack_scheduler.py "${args[@]}" diff --git a/.github/workflows/one-shot-repair-lineageweave-stack.yml b/.github/workflows/one-shot-repair-lineageweave-stack.yml deleted file mode 100644 index d9cfc4304..000000000 --- a/.github/workflows/one-shot-repair-lineageweave-stack.yml +++ /dev/null @@ -1,626 +0,0 @@ -name: One-shot repair LineageWeave stack scheduler - -on: - push: - branches: - - feat/lineageweave-hourly-nvidia-nim-review-repair - paths: - - .github/workflows/one-shot-repair-lineageweave-stack.yml - -permissions: - contents: write - -concurrency: - group: one-shot-repair-lineageweave-stack - cancel-in-progress: false - -jobs: - repair: - runs-on: ubuntu-24.04 - timeout-minutes: 20 - steps: - - name: Checkout exact repair trigger - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - ref: ${{ github.sha }} - fetch-depth: 2 - - name: Verify branch and parent - env: - EXPECTED_BRANCH: feat/lineageweave-hourly-nvidia-nim-review-repair - EXPECTED_PARENT: a633d2af97adf148f1df9f31812d1143bbaa583d - run: | - set -euo pipefail - test "$GITHUB_REF_NAME" = "$EXPECTED_BRANCH" - test "$(git rev-parse HEAD^)" = "$EXPECTED_PARENT" - - name: Apply ordered-stack RED to GREEN repair - run: | - set -euo pipefail - python3 - <<'PY' - from pathlib import Path - - def replace_once(path: str, old: str, new: str) -> None: - target = Path(path) - text = target.read_text(encoding="utf-8") - if text.count(old) != 1: - raise SystemExit(f"expected exactly one match in {path}: {old!r}") - target.write_text(text.replace(old, new), encoding="utf-8") - - scheduler = ".github/workflows/pr-review-fix-scheduler.yml" - replace_once( - scheduler, - ''' base_branch: - description: Base branch to scan; defaults to the caller repository default branch - required: false - default: "" - type: string - canonical_ref:''', - ''' base_branch: - description: Base branch to scan; defaults to the caller repository default branch - required: false - default: "" - type: string - pull_request_numbers: - description: Optional comma-separated PR numbers inspected in the supplied dependency order - required: false - default: "" - type: string - canonical_ref:''', - ) - expression = "$" + "{{ github.event.client_payload.pull_request_numbers || inputs.pull_request_numbers || '' }}" - replace_once( - scheduler, - ' DEFAULT_BRANCH: ${{ github.event.client_payload.base_branch || inputs.base_branch || vars.PR_REVIEW_FIX_BASE_BRANCH || github.event.repository.default_branch }}\n DRY_RUN:', - ' DEFAULT_BRANCH: ${{ github.event.client_payload.base_branch || inputs.base_branch || vars.PR_REVIEW_FIX_BASE_BRANCH || github.event.repository.default_branch }}\n' - f' PULL_REQUEST_NUMBERS: {expression}\n' - ' DRY_RUN:', - ) - replace_once( - scheduler, - ' - name: Self-test fix scheduler contract\n run: python3 scripts/ci/pr_review_fix_scheduler.py --self-test', - ' - name: Self-test fix scheduler contracts\n run: |\n python3 scripts/ci/pr_review_fix_scheduler.py --self-test\n python3 scripts/ci/pr_review_fix_stack_scheduler.py --self-test', - ) - replace_once( - scheduler, - ''' if [ "$RESOLVE_UNREVIEWED_CONFLICTS" = "true" ]; then - args+=(--resolve-unreviewed-conflicts) - fi - if [ "$DRY_RUN" = "true" ]; then - args+=(--dry-run) - fi - python3 scripts/ci/pr_review_fix_scheduler.py "${args[@]}"''', - ''' scheduler_script="scripts/ci/pr_review_fix_scheduler.py" - if [ -n "$PULL_REQUEST_NUMBERS" ]; then - scheduler_script="scripts/ci/pr_review_fix_stack_scheduler.py" - args+=(--pull-request-numbers "$PULL_REQUEST_NUMBERS") - fi - if [ "$RESOLVE_UNREVIEWED_CONFLICTS" = "true" ]; then - args+=(--resolve-unreviewed-conflicts) - fi - if [ "$DRY_RUN" = "true" ]; then - args+=(--dry-run) - fi - python3 "$scheduler_script" "${args[@]}"''', - ) - - Path("scripts/ci/pr_review_fix_stack_scheduler.py").write_text(r'''#!/usr/bin/env python3 - """Dispatch at most one repair across an explicitly ordered pull-request stack.""" - - from __future__ import annotations - - import argparse - import json - import os - import re - from typing import Any - - try: - from pr_review_fix_scheduler import ( - DEFAULT_AUTOFIX_REPOSITORY, - DEFAULT_AUTOFIX_WORKFLOW, - REPO_RE, - fetch_pr, - inspect_pr, - ) - except ModuleNotFoundError: - from scripts.ci.pr_review_fix_scheduler import ( - DEFAULT_AUTOFIX_REPOSITORY, - DEFAULT_AUTOFIX_WORKFLOW, - REPO_RE, - fetch_pr, - inspect_pr, - ) - - BRANCH_RE = re.compile(r"^(?!-)[A-Za-z0-9._/-]+$") - SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") - NUMBER_RE = re.compile(r"^[1-9][0-9]*$") - NO_REPAIR_REASON = ( - "no current-head autofixable review, failed-check RCA, or approved merge conflict" - ) - - - def parse_pull_request_numbers(raw: str, *, maximum: int) -> tuple[int, ...]: - """Return unique positive PR numbers while preserving caller order.""" - - if maximum < 1: - raise ValueError("maximum must be positive") - tokens = tuple(part.strip() for part in raw.split(",") if part.strip()) - if not tokens: - raise ValueError("at least one pull request number is required") - numbers: list[int] = [] - seen: set[int] = set() - for token in tokens: - if not NUMBER_RE.fullmatch(token): - raise ValueError(f"invalid pull request number: {token!r}") - number = int(token) - if number in seen: - raise ValueError(f"duplicate pull request number: {number}") - seen.add(number) - numbers.append(number) - if len(numbers) > maximum: - raise ValueError( - f"pull request stack has {len(numbers)} entries; maximum is {maximum}" - ) - return tuple(numbers) - - - def _single_pull_request(repo: str, number: int) -> dict[str, Any]: - """Fetch exactly one live pull request or fail closed.""" - - records = fetch_pr(repo, number) - if len(records) != 1: - raise RuntimeError( - f"expected one live pull request for #{number}; received {len(records)}" - ) - return records[0] - - - def _validate_stack_edge( - pr: dict[str, Any], - *, - expected_base_name: str, - expected_base_oid: str | None, - ) -> None: - """Require the current child base to equal the declared parent branch and head.""" - - actual_name = str(pr.get("baseRefName") or "") - actual_oid = str(pr.get("baseRefOid") or "") - if actual_name != expected_base_name: - raise RuntimeError( - f"PR #{pr.get('number')} base branch is {actual_name!r}; " - f"expected {expected_base_name!r}" - ) - if expected_base_oid is not None and actual_oid.lower() != expected_base_oid.lower(): - raise RuntimeError( - f"PR #{pr.get('number')} base SHA is {actual_oid or ''}; " - f"expected parent head {expected_base_oid}" - ) - - - def process_stack(args: argparse.Namespace) -> int: - """Inspect the stack in order and stop after one dispatch or blocker.""" - - previous: dict[str, Any] | None = None - inspected = 0 - dispatched = 0 - failed = False - decisions: list[dict[str, Any]] = [] - for number in args.pull_request_numbers: - if dispatched: - break - try: - pr = _single_pull_request(args.repo, number) - expected_name = ( - args.base_branch - if previous is None - else str(previous.get("headRefName") or "") - ) - expected_oid = ( - None - if previous is None - else str(previous.get("headRefOid") or "") - ) - _validate_stack_edge( - pr, - expected_base_name=expected_name, - expected_base_oid=expected_oid, - ) - local_args = argparse.Namespace(**vars(args)) - local_args.base_branch = expected_name - action, reasons = inspect_pr(args.repo, pr, local_args) - except (RuntimeError, ValueError) as exc: - action, reasons = "error", (str(exc),) - inspected += 1 - decisions.append( - {"pr": number, "action": action, "reasons": list(reasons)} - ) - print(f"PR #{number}: {action}: {'; '.join(reasons)}") - if action == "dispatch": - dispatched = 1 - break - if action == "error": - failed = True - break - if action == "wait": - break - if action != "skip" or tuple(reasons) != (NO_REPAIR_REASON,): - break - previous = pr - print( - json.dumps( - { - "inspected": inspected, - "autofix_dispatches": dispatched, - "stack_prs": list(args.pull_request_numbers), - "decisions": decisions, - }, - sort_keys=True, - ) - ) - return 1 if failed else 0 - - - def self_test() -> int: - """Exercise parser invariants without network or repository mutation.""" - - assert parse_pull_request_numbers("258,260,261", maximum=3) == ( - 258, - 260, - 261, - ) - try: - parse_pull_request_numbers("258,258", maximum=3) - except ValueError: - pass - else: # pragma: no cover - defensive contract - raise AssertionError("duplicate PR numbers must fail") - print("stack self-test passed") - return 0 - - - def parse_args(argv: list[str]) -> argparse.Namespace: - """Parse the ordered-stack scheduler CLI contract.""" - - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--repo", default=os.environ.get("GITHUB_REPOSITORY", "")) - parser.add_argument("--base-branch", default=os.environ.get("DEFAULT_BRANCH", "")) - parser.add_argument( - "--pull-request-numbers", - default=os.environ.get("PULL_REQUEST_NUMBERS", ""), - ) - parser.add_argument("--max-prs", type=int, default=50) - parser.add_argument("--max-dispatches", type=int, default=1) - parser.add_argument("--retry-hours", type=int, default=24) - parser.add_argument("--resolve-unreviewed-conflicts", action="store_true") - parser.add_argument("--autofix-workflow", default=DEFAULT_AUTOFIX_WORKFLOW) - parser.add_argument("--autofix-repository", default=DEFAULT_AUTOFIX_REPOSITORY) - parser.add_argument("--dry-run", action="store_true") - parser.add_argument("--self-test", action="store_true") - args = parser.parse_args(argv) - if args.self_test: - return args - if not REPO_RE.fullmatch(args.repo): - parser.error("--repo must be in OWNER/NAME form") - if not BRANCH_RE.fullmatch(args.base_branch): - parser.error("--base-branch is required and must be a safe branch name") - if args.max_prs < 1: - parser.error("--max-prs must be positive") - if args.max_dispatches != 1: - parser.error("ordered stack scheduling requires --max-dispatches 1") - if args.retry_hours < 1: - parser.error("--retry-hours must be positive") - try: - args.pull_request_numbers = parse_pull_request_numbers( - args.pull_request_numbers, - maximum=args.max_prs, - ) - except ValueError as exc: - parser.error(str(exc)) - return args - - - def main(argv: list[str] | None = None) -> int: - """Run the self-test or process one ordered stack.""" - - args = parse_args(list(argv or [])) - if args.self_test: - return self_test() - return process_stack(args) - - - if __name__ == "__main__": # pragma: no cover - raise SystemExit(main()) - ''').replace(" ", ""), encoding="utf-8") - - Path("tests/test_pr_review_fix_stack_scheduler.py").write_text(r'''"""Tests for exact ordered-stack review repair selection.""" - - from __future__ import annotations - - import argparse - import json - - import pytest - - from scripts.ci import pr_review_fix_stack_scheduler as stack - - - def make_pr( - number: int, - *, - base_name: str, - base_oid: str, - head_name: str, - head_oid: str, - ) -> dict: - """Return one minimal scheduler-shaped pull request.""" - - return { - "number": number, - "baseRefName": base_name, - "baseRefOid": base_oid, - "headRefName": head_name, - "headRefOid": head_oid, - } - - - def arguments(numbers: tuple[int, ...]) -> argparse.Namespace: - """Return the runtime arguments consumed by the stack driver.""" - - return argparse.Namespace( - repo="ContextualWisdomLab/LineageWeave", - base_branch="main", - pull_request_numbers=numbers, - max_prs=6, - max_dispatches=1, - retry_hours=2, - resolve_unreviewed_conflicts=True, - autofix_workflow="pr-review-autofix.yml", - autofix_repository="ContextualWisdomLab/.github", - dry_run=False, - ) - - - def test_parse_pull_request_numbers_preserves_order_and_rejects_ambiguity() -> None: - """The explicit queue is positive, unique, bounded, and ordered.""" - - assert stack.parse_pull_request_numbers("258, 260,261", maximum=3) == ( - 258, - 260, - 261, - ) - for raw, maximum in (("", 3), ("0", 3), ("x", 3), ("258,258", 3), ("1,2", 1)): - with pytest.raises(ValueError): - stack.parse_pull_request_numbers(raw, maximum=maximum) - with pytest.raises(ValueError): - stack.parse_pull_request_numbers("1", maximum=0) - - - def test_stack_dispatches_once_in_declared_dependency_order( - monkeypatch, - capsys, - ) -> None: - """A clean parent advances to the first actionable child and then stops.""" - - root = make_pr( - 258, - base_name="main", - base_oid="0" * 40, - head_name="feat/root", - head_oid="1" * 40, - ) - child = make_pr( - 260, - base_name="feat/root", - base_oid="1" * 40, - head_name="feat/child", - head_oid="2" * 40, - ) - grandchild = make_pr( - 261, - base_name="feat/child", - base_oid="2" * 40, - head_name="feat/grandchild", - head_oid="3" * 40, - ) - records = {258: root, 260: child, 261: grandchild} - fetched: list[int] = [] - inspected: list[tuple[int, str]] = [] - - def fake_fetch(repo: str, number: int) -> list[dict]: - assert repo == "ContextualWisdomLab/LineageWeave" - fetched.append(number) - return [records[number]] - - def fake_inspect(repo: str, pr: dict, args: argparse.Namespace): - assert repo == "ContextualWisdomLab/LineageWeave" - inspected.append((pr["number"], args.base_branch)) - if pr["number"] == 258: - return "skip", (stack.NO_REPAIR_REASON,) - return "dispatch", ("current-head OpenCode requested changes",) - - monkeypatch.setattr(stack, "fetch_pr", fake_fetch) - monkeypatch.setattr(stack, "inspect_pr", fake_inspect) - - assert stack.process_stack(arguments((258, 260, 261))) == 0 - assert fetched == [258, 260] - assert inspected == [(258, "main"), (260, "feat/root")] - payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) - assert payload["autofix_dispatches"] == 1 - assert [decision["pr"] for decision in payload["decisions"]] == [258, 260] - - - def test_stack_fails_closed_when_child_is_not_on_exact_parent_head( - monkeypatch, - capsys, - ) -> None: - """A stale descendant cannot be repaired using ancestor evidence.""" - - root = make_pr( - 258, - base_name="main", - base_oid="0" * 40, - head_name="feat/root", - head_oid="1" * 40, - ) - stale_child = make_pr( - 260, - base_name="feat/root", - base_oid="9" * 40, - head_name="feat/child", - head_oid="2" * 40, - ) - records = {258: root, 260: stale_child} - inspected: list[int] = [] - monkeypatch.setattr(stack, "fetch_pr", lambda repo, number: [records[number]]) - - def fake_inspect(repo: str, pr: dict, args: argparse.Namespace): - inspected.append(pr["number"]) - return "skip", (stack.NO_REPAIR_REASON,) - - monkeypatch.setattr(stack, "inspect_pr", fake_inspect) - - assert stack.process_stack(arguments((258, 260))) == 1 - assert inspected == [258] - payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) - assert payload["decisions"][-1]["action"] == "error" - assert "expected parent head" in payload["decisions"][-1]["reasons"][0] - - - def test_stack_stops_on_wait_and_nonrepair_skip(monkeypatch) -> None: - """An in-flight or structurally blocked parent prevents child mutation.""" - - pr = make_pr( - 258, - base_name="main", - base_oid="0" * 40, - head_name="feat/root", - head_oid="1" * 40, - ) - monkeypatch.setattr(stack, "fetch_pr", lambda repo, number: [pr]) - monkeypatch.setattr( - stack, - "inspect_pr", - lambda repo, item, args: ("wait", ("recent autofix marker exists",)), - ) - assert stack.process_stack(arguments((258, 260))) == 0 - - monkeypatch.setattr( - stack, - "inspect_pr", - lambda repo, item, args: ("skip", ("draft PR",)), - ) - assert stack.process_stack(arguments((258, 260))) == 0 - - - def test_stack_handles_missing_pr_and_cli_contract(monkeypatch) -> None: - """Missing records and unsafe CLI values fail before descendant mutation.""" - - monkeypatch.setattr(stack, "fetch_pr", lambda repo, number: []) - assert stack.process_stack(arguments((258,))) == 1 - assert stack.main(["--self-test"]) == 0 - parsed = stack.parse_args( - [ - "--repo", - "ContextualWisdomLab/LineageWeave", - "--base-branch", - "main", - "--pull-request-numbers", - "258,260", - "--max-prs", - "2", - "--max-dispatches", - "1", - ] - ) - assert parsed.pull_request_numbers == (258, 260) - for bad in ( - ["--repo", "bad repo", "--base-branch", "main", "--pull-request-numbers", "258"], - ["--repo", "owner/repo", "--base-branch", "-bad", "--pull-request-numbers", "258"], - ["--repo", "owner/repo", "--base-branch", "main", "--pull-request-numbers", "258", "--max-prs", "0"], - ["--repo", "owner/repo", "--base-branch", "main", "--pull-request-numbers", "258", "--max-dispatches", "2"], - ["--repo", "owner/repo", "--base-branch", "main", "--pull-request-numbers", "258", "--retry-hours", "0"], - ): - with pytest.raises(SystemExit): - stack.parse_args(bad) - ''').replace(" ", ""), encoding="utf-8") - - caller = ".github/workflows/lineageweave-hourly-review-repair.yml" - replace_once( - caller, - ' base_branch: main\n max_prs: "50"', - ' base_branch: main\n pull_request_numbers: "258,260,261,262,263,264"\n max_prs: "6"', - ) - - caller_test = "tests/test_lineageweave_hourly_review_caller.py" - replace_once( - caller_test, - ' assert "base_branch: main" in caller\n assert \'max_prs: "50"\' in caller', - ' assert "base_branch: main" in caller\n assert \'pull_request_numbers: "258,260,261,262,263,264"\' in caller\n assert \'max_prs: "6"\' in caller', - ) - replace_once( - caller_test, - ' ".github/workflows/lineageweave-hourly-review-repair-quality.yml",\n "tests/test_lineageweave_hourly_review_caller.py",', - ' ".github/workflows/lineageweave-hourly-review-repair-quality.yml",\n ".github/workflows/pr-review-fix-scheduler.yml",\n "scripts/ci/pr_review_fix_stack_scheduler.py",\n "tests/test_pr_review_fix_stack_scheduler.py",\n "tests/test_lineageweave_hourly_review_caller.py",', - ) - - quality = ".github/workflows/lineageweave-hourly-review-repair-quality.yml" - for section in ("pull_request", "push"): - marker = f" {section}:\n paths:\n" - text = Path(quality).read_text(encoding="utf-8") - if text.count(marker) != 1: - raise SystemExit(f"missing {section} path marker") - addition = ( - marker - + " - .github/workflows/pr-review-fix-scheduler.yml\n" - + " - scripts/ci/pr_review_fix_stack_scheduler.py\n" - + " - tests/test_pr_review_fix_stack_scheduler.py\n" - ) - Path(quality).write_text(text.replace(marker, addition), encoding="utf-8") - replace_once( - quality, - ' python -m pytest -q tests/test_lineageweave_hourly_review_caller.py\n python -m compileall -q \\\n tests/test_lineageweave_hourly_review_caller.py', - ' python -m pytest -q \\\n tests/test_lineageweave_hourly_review_caller.py \\\n tests/test_pr_review_fix_stack_scheduler.py \\\n tests/test_pr_review_fix_scheduler.py\n python -m compileall -q \\\n scripts/ci/pr_review_fix_stack_scheduler.py \\\n tests/test_lineageweave_hourly_review_caller.py \\\n tests/test_pr_review_fix_stack_scheduler.py', - ) - - doctor = Path("docs/doctoring/lineageweave-hourly-review-caller.md") - text = doctor.read_text(encoding="utf-8") - anchor = "The caller runs at minute 4 of every hour and also exposes `workflow_dispatch`\n" - paragraph = ( - "The caller supplies the explicit ordered queue `258,260,261,262,263,264`. " - "The shared scheduler validates every child base branch and base SHA against " - "the immediately preceding parent head, continues only when the parent has no " - "repairable exact-head evidence, and stops after the first dispatch, wait, or " - "structural blocker.\n\n" - ) - if text.count(anchor) != 1: - raise SystemExit("doctoring insertion anchor missing") - doctor.write_text(text.replace(anchor, paragraph + anchor), encoding="utf-8") - PY - - name: Run focused regression suite - run: | - set -euo pipefail - python -m pytest -q \ - tests/test_lineageweave_hourly_review_caller.py \ - tests/test_pr_review_fix_stack_scheduler.py \ - tests/test_pr_review_fix_scheduler.py - python -m compileall -q \ - scripts/ci/pr_review_fix_stack_scheduler.py \ - tests/test_lineageweave_hourly_review_caller.py \ - tests/test_pr_review_fix_stack_scheduler.py - git diff --check - - name: Commit verified repair and remove one-shot workflow - run: | - set -euo pipefail - rm .github/workflows/one-shot-repair-lineageweave-stack.yml - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add \ - .github/workflows/pr-review-fix-scheduler.yml \ - .github/workflows/lineageweave-hourly-review-repair.yml \ - .github/workflows/lineageweave-hourly-review-repair-quality.yml \ - scripts/ci/pr_review_fix_stack_scheduler.py \ - tests/test_pr_review_fix_stack_scheduler.py \ - tests/test_lineageweave_hourly_review_caller.py \ - docs/doctoring/lineageweave-hourly-review-caller.md \ - .github/workflows/one-shot-repair-lineageweave-stack.yml - git commit -m "fix(automation): traverse LineageWeave stack in dependency order" - git push origin HEAD:feat/lineageweave-hourly-nvidia-nim-review-repair diff --git a/docs/doctoring/lineageweave-buyer-surface-opencode-incident.md b/docs/doctoring/lineageweave-buyer-surface-opencode-incident.md index baf812418..be63990ec 100644 --- a/docs/doctoring/lineageweave-buyer-surface-opencode-incident.md +++ b/docs/doctoring/lineageweave-buyer-surface-opencode-incident.md @@ -46,9 +46,13 @@ The central repair must: - retain exact repository, pull request, head, base, actor, source comment, and review-only behavior binding. -The dedicated LineageWeave hourly caller complements the mention path. It gives -all current heads in the stack a bounded repair heartbeat while keeping the -shared scheduler product-neutral. +The dedicated LineageWeave hourly caller complements the mention path. Initial +OpenCode review generation remains with the mention router and organization +review/merge scheduler. The hourly caller handles only actionable exact-head +review feedback, failed-check RCA, and conflict repair. It inspects the explicit +six-PR queue in dependency order, advances only after an exact no-repair result, +waits without mutation when a child is not based on its current parent head, and +dispatches no more than one repair. ## Stack order diff --git a/docs/doctoring/lineageweave-hourly-review-caller.md b/docs/doctoring/lineageweave-hourly-review-caller.md index a0dadc34f..767485cb4 100644 --- a/docs/doctoring/lineageweave-hourly-review-caller.md +++ b/docs/doctoring/lineageweave-hourly-review-caller.md @@ -6,13 +6,18 @@ ContextualWisdomLab operates one protected hourly caller for `ContextualWisdomLab/LineageWeave`. The caller covers the current buyer-surface -stack **#258 → #260 → #261 → #262 → #263 → #264**, delegates to the -product-neutral central review-fix scheduler, inspects at most 50 open pull -requests targeting protected `main`, and dispatches at most one bounded repair -per heartbeat. +stack **#258 → #260 → #261 → #262 → #263 → #264**, passes that explicit queue +to a product-neutral ordered-stack repair driver, and dispatches at most one +bounded current-head repair per heartbeat. + +The initial formal OpenCode generation remains owned by the central mention +router and the organization review/merge scheduler. This caller begins only +when exact-head OpenCode feedback, a failed-check root-cause review, or an +approved/unreviewed merge-conflict repair is actionable. It never treats a +missing initial review as repair evidence. The caller runs at minute 4 of every hour and also exposes `workflow_dispatch` -for an operator-controlled acceptance run. It does not contain product mutation +for an operator-controlled acceptance run. It does not contain product edit logic, LLM credentials, approval authority, merge authority, or release authority. LineageWeave remains independently deployable; privileged automation remains in `ContextualWisdomLab/.github`. @@ -67,10 +72,17 @@ requests: → #264 ``` -A child is reviewed against its exact declared parent head. When a parent moves, -the child is stale until its base is updated and all exact-head checks and -reviews are regenerated. A green result from an ancestor, predecessor head, or -sibling cannot satisfy a descendant gate. +The driver refetches every PR in declared order. A child must target the +immediately preceding parent branch, and its base SHA must equal that parent's +exact current head. A branch-name mismatch fails closed as a structural error. +A stale base SHA is a non-mutating wait that names the required restack. The +review/merge scheduler owns branch updates; the repair caller never edits a +stale descendant against obsolete evidence. + +Only the exact no-repair result permits the driver to advance to the next child. +A draft parent, recent same-head marker, active repair, structural error, or +other blocker stops the pass. A green result from an ancestor, predecessor head, +or sibling cannot satisfy a descendant gate. ## Cadence and concurrency @@ -95,11 +107,11 @@ changing the cadence or credential boundary. ## Credential and model boundary -The caller keeps workflow `GITHUB_TOKEN` at `contents: read`. Only the reusable -job receives `id-token: write`, allowing the reviewed central scheduler to use -its established GitHub OIDC credential path when required. The caller maps only -`PR_REVIEW_MERGE_TOKEN` and `OPENCODE_APPROVE_TOKEN`; it never uses -`secrets: inherit`, receives `NVIDIA_NIM_API_KEY`, or introduces +The caller keeps workflow `GITHUB_TOKEN` at `contents: read`. Its one job receives +`id-token: write`, allowing the reviewed OpenCode GitHub App exchange when an +established user token is absent. It accepts only `PR_REVIEW_MERGE_TOKEN`, +`OPENCODE_APPROVE_TOKEN`, or that exchanged app token as mutation authority. It +never uses `secrets: inherit`, receives `NVIDIA_NIM_API_KEY`, or introduces `COPILOT_GITHUB_TOKEN`. Model execution remains inside the separately reviewed central worker. Before @@ -135,19 +147,24 @@ receipt, or a green status alone is not a review verdict. Machine-checkable contracts require: -- exact `ContextualWisdomLab/LineageWeave` target and protected `main` base; +- exact `ContextualWisdomLab/LineageWeave` target and protected `main` source; +- explicit ordered queue `258,260,261,262,263,264`; +- exact child branch and parent-head validation; +- stale-child wait without mutation; - minute 4 hourly cadence plus a manual acceptance entry point; - non-cancelling repository-scoped concurrency; - at most one dispatch and a two-hour same-head retry floor; - read-only workflow contents plus job-scoped OIDC; -- explicit scheduler-secret mapping; -- absence of model, Copilot, merge, release, and target-mutation authority; -- a focused path-filtered quality workflow for caller, test, and doctoring; and -- no product identity hard-coded in the reusable scheduler. - -Rollback removes only this caller, its focused quality workflow, its contract -test, and its doctoring records. It must not remove the product-neutral -scheduler, the central mention-router repair, or another product caller. +- explicit scheduler-secret or app-token selection; +- absence of model, Copilot, merge, release, and target-setting authority; +- production statement and branch coverage 100% for the stack driver; +- public API docstring coverage 100%; and +- a focused path-filtered quality workflow for caller, driver, tests, and doctoring. + +Rollback removes only this caller, its focused quality workflow, the product-neutral +stack driver if no other caller consumes it, its tests, and its doctoring records. +It must not remove the central mention-router repair, the organization +review/merge scheduler, or another product caller. ## APA 7th references diff --git a/scripts/ci/pr_review_fix_stack_scheduler.py b/scripts/ci/pr_review_fix_stack_scheduler.py new file mode 100644 index 000000000..e2c49e06a --- /dev/null +++ b/scripts/ci/pr_review_fix_stack_scheduler.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +"""Dispatch at most one repair across an explicitly ordered pull-request stack.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +from typing import Any + +try: + from pr_review_fix_scheduler import ( + DEFAULT_AUTOFIX_REPOSITORY, + DEFAULT_AUTOFIX_WORKFLOW, + REPO_RE, + fetch_pr, + inspect_pr, + ) +except ModuleNotFoundError: + from scripts.ci.pr_review_fix_scheduler import ( + DEFAULT_AUTOFIX_REPOSITORY, + DEFAULT_AUTOFIX_WORKFLOW, + REPO_RE, + fetch_pr, + inspect_pr, + ) + +BRANCH_RE = re.compile(r"^(?!-)[A-Za-z0-9._/-]+$") +NUMBER_RE = re.compile(r"^[1-9][0-9]*$") +NO_REPAIR_REASON = ( + "no current-head autofixable review, failed-check RCA, or approved merge conflict" +) + + +def parse_pull_request_numbers(raw: str, *, maximum: int) -> tuple[int, ...]: + """Return unique positive PR numbers while preserving caller order.""" + + if maximum < 1: + raise ValueError("maximum must be positive") + tokens = tuple(part.strip() for part in raw.split(",") if part.strip()) + if not tokens: + raise ValueError("at least one pull request number is required") + numbers: list[int] = [] + seen: set[int] = set() + for token in tokens: + if not NUMBER_RE.fullmatch(token): + raise ValueError(f"invalid pull request number: {token!r}") + number = int(token) + if number in seen: + raise ValueError(f"duplicate pull request number: {number}") + seen.add(number) + numbers.append(number) + if len(numbers) > maximum: + raise ValueError( + f"pull request stack has {len(numbers)} entries; maximum is {maximum}" + ) + return tuple(numbers) + + +def _single_pull_request(repo: str, number: int) -> dict[str, Any]: + """Fetch exactly one pull request snapshot or fail closed.""" + + records = fetch_pr(repo, number) + if len(records) != 1: + raise RuntimeError( + f"expected one pull request for #{number}; received {len(records)}" + ) + return records[0] + + +def _base_branch_error( + pr: dict[str, Any], + *, + expected_base_name: str, +) -> str | None: + """Return a structural error when a child targets the wrong parent branch.""" + + actual_name = str(pr.get("baseRefName") or "") + if actual_name == expected_base_name: + return None + return ( + f"PR #{pr.get('number')} base branch is {actual_name!r}; " + f"expected {expected_base_name!r}" + ) + + +def _stale_base_reason( + pr: dict[str, Any], + *, + expected_base_oid: str | None, +) -> str | None: + """Return a wait reason when a child is not based on the current parent head.""" + + if expected_base_oid is None: + return None + actual_oid = str(pr.get("baseRefOid") or "") + if actual_oid.lower() == expected_base_oid.lower(): + return None + return ( + f"PR #{pr.get('number')} base SHA is {actual_oid or ''}; " + f"expected parent head {expected_base_oid}; restack before descendant repair" + ) + + +def _summary( + *, + inspected: int, + dispatched: int, + pull_request_numbers: tuple[int, ...], + decisions: list[dict[str, Any]], +) -> str: + """Serialize one deterministic machine-readable stack decision summary.""" + + return json.dumps( + { + "inspected": inspected, + "autofix_dispatches": dispatched, + "stack_prs": list(pull_request_numbers), + "decisions": decisions, + }, + sort_keys=True, + ) + + +def process_stack(args: argparse.Namespace) -> int: + """Inspect the stack in order and stop after one dispatch or blocker.""" + + previous: dict[str, Any] | None = None + inspected = 0 + dispatched = 0 + failed = False + decisions: list[dict[str, Any]] = [] + for number in args.pull_request_numbers: + try: + pr = _single_pull_request(args.repo, number) + except RuntimeError as exc: + action, reasons = "error", (str(exc),) + else: + expected_name = ( + args.base_branch + if previous is None + else str(previous.get("headRefName") or "") + ) + expected_oid = ( + None + if previous is None + else str(previous.get("headRefOid") or "") + ) + branch_error = _base_branch_error( + pr, + expected_base_name=expected_name, + ) + stale_reason = _stale_base_reason( + pr, + expected_base_oid=expected_oid, + ) + if branch_error is not None: + action, reasons = "error", (branch_error,) + elif stale_reason is not None: + action, reasons = "wait", (stale_reason,) + else: + local_args = argparse.Namespace(**vars(args)) + local_args.base_branch = expected_name + try: + action, reasons = inspect_pr(args.repo, pr, local_args) + except (RuntimeError, ValueError) as exc: + action, reasons = "error", (str(exc),) + + inspected += 1 + decisions.append( + {"pr": number, "action": action, "reasons": list(reasons)} + ) + print(f"PR #{number}: {action}: {'; '.join(reasons)}") + if action == "dispatch": + dispatched = 1 + break + if action == "error": + failed = True + break + if action == "wait": + break + if action != "skip" or tuple(reasons) != (NO_REPAIR_REASON,): + break + previous = pr + + print( + _summary( + inspected=inspected, + dispatched=dispatched, + pull_request_numbers=args.pull_request_numbers, + decisions=decisions, + ) + ) + return 1 if failed else 0 + + +def self_test() -> int: + """Exercise parser invariants without network or repository mutation.""" + + assert parse_pull_request_numbers("1,2,3", maximum=3) == (1, 2, 3) + try: + parse_pull_request_numbers("1,1", maximum=3) + except ValueError: + pass + else: # pragma: no cover - defensive contract + raise AssertionError("duplicate PR numbers must fail") + print("stack self-test passed") + return 0 + + +def parse_args(argv: list[str]) -> argparse.Namespace: + """Parse the ordered-stack scheduler CLI contract.""" + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--repo", default=os.environ.get("GITHUB_REPOSITORY", "")) + parser.add_argument("--base-branch", default=os.environ.get("DEFAULT_BRANCH", "")) + parser.add_argument( + "--pull-request-numbers", + default=os.environ.get("PULL_REQUEST_NUMBERS", ""), + ) + parser.add_argument("--max-prs", type=int, default=50) + parser.add_argument("--max-dispatches", type=int, default=1) + parser.add_argument("--retry-hours", type=int, default=24) + parser.add_argument("--resolve-unreviewed-conflicts", action="store_true") + parser.add_argument("--autofix-workflow", default=DEFAULT_AUTOFIX_WORKFLOW) + parser.add_argument("--autofix-repository", default=DEFAULT_AUTOFIX_REPOSITORY) + parser.add_argument("--dry-run", action="store_true") + parser.add_argument("--self-test", action="store_true") + args = parser.parse_args(argv) + if args.self_test: + return args + if not REPO_RE.fullmatch(args.repo): + parser.error("--repo must be in OWNER/NAME form") + if not BRANCH_RE.fullmatch(args.base_branch): + parser.error("--base-branch is required and must be a safe branch name") + if args.max_prs < 1: + parser.error("--max-prs must be positive") + if args.max_dispatches != 1: + parser.error("ordered stack scheduling requires --max-dispatches 1") + if args.retry_hours < 1: + parser.error("--retry-hours must be positive") + try: + args.pull_request_numbers = parse_pull_request_numbers( + args.pull_request_numbers, + maximum=args.max_prs, + ) + except ValueError as exc: + parser.error(str(exc)) + return args + + +def main(argv: list[str] | None = None) -> int: + """Run the self-test or process one ordered stack.""" + + args = parse_args(sys.argv[1:] if argv is None else argv) + if args.self_test: + return self_test() + return process_stack(args) + + +if __name__ == "__main__": # pragma: no cover + raise SystemExit(main()) diff --git a/tests/test_lineageweave_hourly_review_caller.py b/tests/test_lineageweave_hourly_review_caller.py index 459980d30..1fb47d498 100644 --- a/tests/test_lineageweave_hourly_review_caller.py +++ b/tests/test_lineageweave_hourly_review_caller.py @@ -7,9 +7,10 @@ QUALITY_WORKFLOW = Path( ".github/workflows/lineageweave-hourly-review-repair-quality.yml" ) +STACK_DRIVER = Path("scripts/ci/pr_review_fix_stack_scheduler.py") DOCTORING = Path("docs/doctoring/lineageweave-hourly-review-caller.md") INCIDENT = Path("docs/doctoring/lineageweave-buyer-surface-opencode-incident.md") -SCHEDULER = Path(".github/workflows/pr-review-fix-scheduler.yml") +ONE_SHOT = Path(".github/workflows/one-shot-repair-lineageweave-stack.yml") def _read(path: Path) -> str: @@ -18,8 +19,8 @@ def _read(path: Path) -> str: return path.read_text(encoding="utf-8") -def test_lineageweave_caller_is_manual_hourly_bounded_and_non_cancelling() -> None: - """LineageWeave receives one bounded repair without cancelling live RCA work.""" +def test_lineageweave_caller_is_manual_hourly_bounded_and_ordered() -> None: + """The heartbeat covers all six PRs and permits one dependency-safe repair.""" caller = _read(CALLER) @@ -27,27 +28,30 @@ def test_lineageweave_caller_is_manual_hourly_bounded_and_non_cancelling() -> No assert 'cron: "4 * * * *"' in caller assert "group: lineageweave-hourly-review-repair" in caller assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/LineageWeave" in caller - assert "base_branch: main" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert 'retry_hours: "2"' in caller + assert "TARGET_REPOSITORY: ContextualWisdomLab/LineageWeave" in caller + assert "ROOT_BASE_BRANCH: main" in caller + assert 'PULL_REQUEST_NUMBERS: "258,260,261,262,263,264"' in caller + assert 'MAX_PRS: "6"' in caller + assert 'MAX_DISPATCHES: "1"' in caller + assert 'RETRY_HOURS: "2"' in caller + assert "pr_review_fix_stack_scheduler.py" in caller + assert "--pull-request-numbers \"$PULL_REQUEST_NUMBERS\"" in caller -def test_lineageweave_caller_preserves_oidc_and_explicit_secret_scope() -> None: - """The caller maps established credentials without model or mutation scope.""" +def test_lineageweave_caller_is_protected_main_only_and_least_privilege() -> None: + """Only protected central main can materialize the established mutation path.""" caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - assert "\npermissions:\n contents: read\n" in workflow_scope - assert ( - "\n permissions:\n contents: read\n id-token: write\n" - in jobs_scope - ) - assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller - assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller + assert "contents: read" in caller + assert "id-token: write" in caller + assert 'GITHUB_REF" != "refs/heads/main"' in caller + assert "OPENCODE_REPOSITORY_DISPATCH_TARGETS" in caller + assert "PR_REVIEW_MERGE_TOKEN" in caller + assert "OPENCODE_APPROVE_TOKEN" in caller + assert "persist-credentials: false" in caller + assert "--connect-timeout 10" in caller + assert "--max-time 30" in caller assert "secrets: inherit" not in caller assert "NVIDIA_NIM_API_KEY" not in caller assert "COPILOT_GITHUB_TOKEN" not in caller @@ -61,19 +65,26 @@ def test_lineageweave_caller_preserves_oidc_and_explicit_secret_scope() -> None: assert forbidden not in caller -def test_lineageweave_target_is_not_hard_coded_in_shared_scheduler() -> None: - """Product identity remains in the thin caller rather than the engine.""" +def test_stack_driver_is_product_neutral_and_one_shot_is_absent() -> None: + """LineageWeave identity and PR numbers remain in the thin caller only.""" - assert "ContextualWisdomLab/LineageWeave" not in _read(SCHEDULER) + driver = _read(STACK_DRIVER) + assert "ContextualWisdomLab/LineageWeave" not in driver + for number in ("258", "260", "261", "262", "263", "264"): + assert number not in driver + assert "expected parent head" in driver + assert "max_dispatches != 1" in driver + assert not ONE_SHOT.exists() def test_lineageweave_doctoring_preserves_operational_boundaries() -> None: - """The original doctoring retains target, credential, and approval rules.""" + """Doctoring retains target, credential, stack, and approval rules.""" doctoring = _read(DOCTORING) for phrase in ( "ContextualWisdomLab/LineageWeave", + "#258 → #260 → #261 → #262 → #263 → #264", "OPENCODE_REPOSITORY_DISPATCH_TARGETS", "independent non-author approval", "NVIDIA_NIM_API_KEY", @@ -109,12 +120,14 @@ def test_incident_doctoring_tracks_current_buyer_surface_stack() -> None: def test_focused_quality_workflow_tracks_every_owned_contract() -> None: - """Caller, quality, test, and both doctoring records rerun the focused gate.""" + """Caller, driver, tests, and doctoring edits rerun the focused gate.""" quality = _read(QUALITY_WORKFLOW) owned_paths = ( ".github/workflows/lineageweave-hourly-review-repair.yml", ".github/workflows/lineageweave-hourly-review-repair-quality.yml", + "scripts/ci/pr_review_fix_stack_scheduler.py", + "tests/test_pr_review_fix_stack_scheduler.py", "tests/test_lineageweave_hourly_review_caller.py", "docs/doctoring/lineageweave-hourly-review-caller.md", "docs/doctoring/lineageweave-buyer-surface-opencode-incident.md", @@ -124,7 +137,10 @@ def test_focused_quality_workflow_tracks_every_owned_contract() -> None: assert "cancel-in-progress: true" in quality assert "persist-credentials: false" in quality assert "--require-hashes" in quality - assert "tests/test_lineageweave_hourly_review_caller.py" in quality + assert "tests/test_pr_review_fix_stack_scheduler.py" in quality + assert "tests/test_pr_review_fix_scheduler.py" in quality + assert "--fail-under=100" in quality + assert "interrogate -vv --fail-under 100" in quality assert "python -m compileall -q \\" in quality assert "git diff --check" in quality for path in owned_paths: diff --git a/tests/test_pr_review_fix_stack_scheduler.py b/tests/test_pr_review_fix_stack_scheduler.py new file mode 100644 index 000000000..c3cc4bacc --- /dev/null +++ b/tests/test_pr_review_fix_stack_scheduler.py @@ -0,0 +1,371 @@ +"""Tests for exact ordered-stack review repair selection.""" + +from __future__ import annotations + +import argparse +import json + +import pytest + +from scripts.ci import pr_review_fix_stack_scheduler as stack + + +def make_pr( + number: int, + *, + base_name: str, + base_oid: str, + head_name: str, + head_oid: str, +) -> dict: + """Return one minimal scheduler-shaped pull request.""" + + return { + "number": number, + "baseRefName": base_name, + "baseRefOid": base_oid, + "headRefName": head_name, + "headRefOid": head_oid, + } + + +def arguments(numbers: tuple[int, ...]) -> argparse.Namespace: + """Return runtime arguments consumed by the stack driver.""" + + return argparse.Namespace( + repo="ContextualWisdomLab/LineageWeave", + base_branch="main", + pull_request_numbers=numbers, + max_prs=6, + max_dispatches=1, + retry_hours=2, + resolve_unreviewed_conflicts=True, + autofix_workflow="pr-review-autofix.yml", + autofix_repository="ContextualWisdomLab/.github", + dry_run=False, + ) + + +def test_parse_pull_request_numbers_preserves_order_and_rejects_ambiguity() -> None: + """The explicit queue is positive, unique, bounded, and ordered.""" + + assert stack.parse_pull_request_numbers("258, 260,261", maximum=3) == ( + 258, + 260, + 261, + ) + for raw, maximum in ( + ("", 3), + ("0", 3), + ("x", 3), + ("258,258", 3), + ("1,2", 1), + ): + with pytest.raises(ValueError): + stack.parse_pull_request_numbers(raw, maximum=maximum) + with pytest.raises(ValueError): + stack.parse_pull_request_numbers("1", maximum=0) + + +def test_stack_dispatches_once_in_declared_dependency_order( + monkeypatch, + capsys, +) -> None: + """A clean parent advances to the first actionable child and then stops.""" + + root = make_pr( + 258, + base_name="main", + base_oid="0" * 40, + head_name="feat/root", + head_oid="1" * 40, + ) + child = make_pr( + 260, + base_name="feat/root", + base_oid="1" * 40, + head_name="feat/child", + head_oid="2" * 40, + ) + grandchild = make_pr( + 261, + base_name="feat/child", + base_oid="2" * 40, + head_name="feat/grandchild", + head_oid="3" * 40, + ) + records = {258: root, 260: child, 261: grandchild} + fetched: list[int] = [] + inspected: list[tuple[int, str]] = [] + + def fake_fetch(repo: str, number: int) -> list[dict]: + assert repo == "ContextualWisdomLab/LineageWeave" + fetched.append(number) + return [records[number]] + + def fake_inspect(repo: str, pr: dict, args: argparse.Namespace): + assert repo == "ContextualWisdomLab/LineageWeave" + inspected.append((pr["number"], args.base_branch)) + if pr["number"] == 258: + return "skip", (stack.NO_REPAIR_REASON,) + return "dispatch", ("current-head OpenCode requested changes",) + + monkeypatch.setattr(stack, "fetch_pr", fake_fetch) + monkeypatch.setattr(stack, "inspect_pr", fake_inspect) + + assert stack.process_stack(arguments((258, 260, 261))) == 0 + assert fetched == [258, 260] + assert inspected == [(258, "main"), (260, "feat/root")] + payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) + assert payload["autofix_dispatches"] == 1 + assert [decision["pr"] for decision in payload["decisions"]] == [258, 260] + + +def test_stack_waits_when_child_base_sha_is_stale(monkeypatch, capsys) -> None: + """A stale descendant waits for restacking instead of failing or mutating.""" + + root = make_pr( + 258, + base_name="main", + base_oid="0" * 40, + head_name="feat/root", + head_oid="1" * 40, + ) + stale_child = make_pr( + 260, + base_name="feat/root", + base_oid="9" * 40, + head_name="feat/child", + head_oid="2" * 40, + ) + records = {258: root, 260: stale_child} + inspected: list[int] = [] + monkeypatch.setattr(stack, "fetch_pr", lambda repo, number: [records[number]]) + + def fake_inspect(repo: str, pr: dict, args: argparse.Namespace): + inspected.append(pr["number"]) + return "skip", (stack.NO_REPAIR_REASON,) + + monkeypatch.setattr(stack, "inspect_pr", fake_inspect) + + assert stack.process_stack(arguments((258, 260))) == 0 + assert inspected == [258] + payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) + assert payload["decisions"][-1]["action"] == "wait" + assert "expected parent head" in payload["decisions"][-1]["reasons"][0] + + +def test_stack_fails_closed_on_wrong_parent_branch(monkeypatch, capsys) -> None: + """A child targeting an unexpected branch is a structural contract failure.""" + + root = make_pr( + 258, + base_name="main", + base_oid="0" * 40, + head_name="feat/root", + head_oid="1" * 40, + ) + wrong_child = make_pr( + 260, + base_name="feat/other", + base_oid="1" * 40, + head_name="feat/child", + head_oid="2" * 40, + ) + records = {258: root, 260: wrong_child} + monkeypatch.setattr(stack, "fetch_pr", lambda repo, number: [records[number]]) + monkeypatch.setattr( + stack, + "inspect_pr", + lambda repo, pr, args: ("skip", (stack.NO_REPAIR_REASON,)), + ) + + assert stack.process_stack(arguments((258, 260))) == 1 + payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) + assert payload["decisions"][-1]["action"] == "error" + assert "expected 'feat/root'" in payload["decisions"][-1]["reasons"][0] + + +def test_stack_stops_on_wait_draft_and_nonrepair_skip(monkeypatch) -> None: + """An in-flight or structurally blocked parent prevents descendant repair.""" + + pr = make_pr( + 258, + base_name="main", + base_oid="0" * 40, + head_name="feat/root", + head_oid="1" * 40, + ) + fetched: list[int] = [] + + def fake_fetch(repo: str, number: int) -> list[dict]: + fetched.append(number) + return [pr] + + monkeypatch.setattr(stack, "fetch_pr", fake_fetch) + monkeypatch.setattr( + stack, + "inspect_pr", + lambda repo, item, args: ("wait", ("recent autofix marker exists",)), + ) + assert stack.process_stack(arguments((258, 260))) == 0 + assert fetched == [258] + + fetched.clear() + monkeypatch.setattr( + stack, + "inspect_pr", + lambda repo, item, args: ("skip", ("draft PR",)), + ) + assert stack.process_stack(arguments((258, 260))) == 0 + assert fetched == [258] + + +def test_stack_handles_missing_pr_and_cli_contract(monkeypatch) -> None: + """Missing records and unsafe CLI values fail before descendant mutation.""" + + monkeypatch.setattr(stack, "fetch_pr", lambda repo, number: []) + assert stack.process_stack(arguments((258,))) == 1 + assert stack.main(["--self-test"]) == 0 + parsed = stack.parse_args( + [ + "--repo", + "ContextualWisdomLab/LineageWeave", + "--base-branch", + "main", + "--pull-request-numbers", + "258,260", + "--max-prs", + "2", + "--max-dispatches", + "1", + ] + ) + assert parsed.pull_request_numbers == (258, 260) + for bad in ( + [ + "--repo", + "bad repo", + "--base-branch", + "main", + "--pull-request-numbers", + "258", + ], + [ + "--repo", + "owner/repo", + "--base-branch", + "-bad", + "--pull-request-numbers", + "258", + ], + [ + "--repo", + "owner/repo", + "--base-branch", + "main", + "--pull-request-numbers", + "258", + "--max-prs", + "0", + ], + [ + "--repo", + "owner/repo", + "--base-branch", + "main", + "--pull-request-numbers", + "258", + "--max-dispatches", + "2", + ], + [ + "--repo", + "owner/repo", + "--base-branch", + "main", + "--pull-request-numbers", + "258", + "--retry-hours", + "0", + ], + ): + with pytest.raises(SystemExit): + stack.parse_args(bad) + + +def test_stack_handles_inspection_failure_and_empty_queue( + monkeypatch, + capsys, +) -> None: + """Inspection errors fail closed and an already-empty queue reports cleanly.""" + + pr = make_pr( + 258, + base_name="main", + base_oid="0" * 40, + head_name="feat/root", + head_oid="1" * 40, + ) + monkeypatch.setattr(stack, "fetch_pr", lambda repo, number: [pr]) + monkeypatch.setattr( + stack, + "inspect_pr", + lambda repo, item, args: (_ for _ in ()).throw(RuntimeError("boom")), + ) + assert stack.process_stack(arguments((258,))) == 1 + payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) + assert payload["decisions"][0]["reasons"] == ["boom"] + + empty_args = arguments(()) + assert stack.process_stack(empty_args) == 0 + payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) + assert payload["inspected"] == 0 + + +def test_cli_covers_invalid_base_stack_value_and_normal_main(monkeypatch) -> None: + """CLI validation reports unsafe values and main delegates normal execution.""" + + with pytest.raises(SystemExit): + stack.parse_args( + [ + "--repo", + "owner/repo", + "--base-branch", + "", + "--pull-request-numbers", + "258", + ] + ) + with pytest.raises(SystemExit): + stack.parse_args( + [ + "--repo", + "owner/repo", + "--base-branch", + "main", + "--pull-request-numbers", + "bad", + ] + ) + + seen: list[tuple[int, ...]] = [] + monkeypatch.setattr( + stack, + "process_stack", + lambda args: seen.append(args.pull_request_numbers) or 7, + ) + assert ( + stack.main( + [ + "--repo", + "owner/repo", + "--base-branch", + "main", + "--pull-request-numbers", + "258", + ] + ) + == 7 + ) + assert seen == [(258,)] From 6e9b9883eb36772e99b43f6169670a26c0d9383c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 15:04:46 +0900 Subject: [PATCH 09/20] fix(lineageweave): fail closed on moving stack metadata --- scripts/ci/pr_review_fix_stack_scheduler.py | 79 ++++++++-- tests/test_pr_review_fix_stack_scheduler.py | 157 +++++++++++++++++++- 2 files changed, 226 insertions(+), 10 deletions(-) diff --git a/scripts/ci/pr_review_fix_stack_scheduler.py b/scripts/ci/pr_review_fix_stack_scheduler.py index e2c49e06a..8d8483613 100644 --- a/scripts/ci/pr_review_fix_stack_scheduler.py +++ b/scripts/ci/pr_review_fix_stack_scheduler.py @@ -29,6 +29,9 @@ BRANCH_RE = re.compile(r"^(?!-)[A-Za-z0-9._/-]+$") NUMBER_RE = re.compile(r"^[1-9][0-9]*$") +SHA_RE = re.compile(r"^[0-9a-fA-F]{40}$") +MAX_BRANCH_NAME_LENGTH = 255 +VALID_ACTIONS = frozenset({"dispatch", "error", "skip", "wait"}) NO_REPAIR_REASON = ( "no current-head autofixable review, failed-check RCA, or approved merge conflict" ) @@ -39,8 +42,8 @@ def parse_pull_request_numbers(raw: str, *, maximum: int) -> tuple[int, ...]: if maximum < 1: raise ValueError("maximum must be positive") - tokens = tuple(part.strip() for part in raw.split(",") if part.strip()) - if not tokens: + tokens = tuple(part.strip() for part in raw.split(",")) + if not tokens or any(not token for token in tokens): raise ValueError("at least one pull request number is required") numbers: list[int] = [] seen: set[int] = set() @@ -63,11 +66,47 @@ def _single_pull_request(repo: str, number: int) -> dict[str, Any]: """Fetch exactly one pull request snapshot or fail closed.""" records = fetch_pr(repo, number) + if not isinstance(records, list): + raise RuntimeError("pull request response must be a list") if len(records) != 1: raise RuntimeError( f"expected one pull request for #{number}; received {len(records)}" ) - return records[0] + record = records[0] + if not isinstance(record, dict): + raise RuntimeError("pull request response item must be an object") + if type(record.get("number")) is not int or record["number"] != number: + raise RuntimeError(f"pull request response number does not match #{number}") + for field in ("baseRefName", "headRefName"): + branch_name = record.get(field) + if ( + not isinstance(branch_name, str) + or len(branch_name) > MAX_BRANCH_NAME_LENGTH + or ".." in branch_name + or not BRANCH_RE.fullmatch(branch_name) + ): + raise RuntimeError(f"pull request response has an unsafe {field}") + for field in ("baseRefOid", "headRefOid"): + if not isinstance(record.get(field), str) or not SHA_RE.fullmatch(record[field]): + raise RuntimeError(f"pull request response has an invalid {field}") + return record + + +def _normalize_decision( + decision: object, +) -> tuple[str, tuple[str, ...]]: + """Validate a shared scheduler decision before recording or acting on it.""" + + if not isinstance(decision, tuple) or len(decision) != 2: + return "error", ("shared scheduler returned a malformed decision",) + action, reasons = decision + if not isinstance(action, str) or action not in VALID_ACTIONS: + return "error", ("shared scheduler returned an unknown action",) + if not isinstance(reasons, (tuple, list)) or not reasons: + return "error", ("shared scheduler returned empty reasons",) + if any(not isinstance(reason, str) or not reason.strip() for reason in reasons): + return "error", ("shared scheduler returned non-string reasons",) + return str(action), tuple(reason.strip() for reason in reasons) def _base_branch_error( @@ -128,16 +167,35 @@ def process_stack(args: argparse.Namespace) -> int: """Inspect the stack in order and stop after one dispatch or blocker.""" previous: dict[str, Any] | None = None + previous_number: int | None = None inspected = 0 dispatched = 0 failed = False decisions: list[dict[str, Any]] = [] for number in args.pull_request_numbers: - try: - pr = _single_pull_request(args.repo, number) - except RuntimeError as exc: - action, reasons = "error", (str(exc),) - else: + pr: dict[str, Any] | None = None + action = "" + reasons: tuple[str, ...] = () + if previous is not None and previous_number is not None: + try: + refreshed_parent = _single_pull_request(args.repo, previous_number) + except RuntimeError as exc: + action, reasons = "error", (str(exc),) + else: + if refreshed_parent["headRefOid"].lower() != previous["headRefOid"].lower(): + action, reasons = "wait", ( + f"parent PR #{previous_number} head moved from " + f"{previous['headRefOid']} to {refreshed_parent['headRefOid']}; " + "restack before descendant repair", + ) + else: + previous = refreshed_parent + if not action: + try: + pr = _single_pull_request(args.repo, number) + except RuntimeError as exc: + action, reasons = "error", (str(exc),) + if not action: expected_name = ( args.base_branch if previous is None @@ -164,7 +222,9 @@ def process_stack(args: argparse.Namespace) -> int: local_args = argparse.Namespace(**vars(args)) local_args.base_branch = expected_name try: - action, reasons = inspect_pr(args.repo, pr, local_args) + action, reasons = _normalize_decision( + inspect_pr(args.repo, pr, local_args) + ) except (RuntimeError, ValueError) as exc: action, reasons = "error", (str(exc),) @@ -184,6 +244,7 @@ def process_stack(args: argparse.Namespace) -> int: if action != "skip" or tuple(reasons) != (NO_REPAIR_REASON,): break previous = pr + previous_number = number print( _summary( diff --git a/tests/test_pr_review_fix_stack_scheduler.py b/tests/test_pr_review_fix_stack_scheduler.py index c3cc4bacc..047e2670d 100644 --- a/tests/test_pr_review_fix_stack_scheduler.py +++ b/tests/test_pr_review_fix_stack_scheduler.py @@ -59,6 +59,9 @@ def test_parse_pull_request_numbers_preserves_order_and_rejects_ambiguity() -> N ("0", 3), ("x", 3), ("258,258", 3), + ("258,,260", 3), + (",258", 3), + ("258,", 3), ("1,2", 1), ): with pytest.raises(ValueError): @@ -67,6 +70,50 @@ def test_parse_pull_request_numbers_preserves_order_and_rejects_ambiguity() -> N stack.parse_pull_request_numbers("1", maximum=0) +@pytest.mark.parametrize( + "response", + [ + ("not-a-list", "list"), + ([], "one pull request"), + ([None], "object"), + ([{"number": 259}], "number"), + ( + [ + { + "number": 258, + "baseRefName": "main", + "headRefName": "x" * 256, + "baseRefOid": "0" * 40, + "headRefOid": "1" * 40, + } + ], + "headRefName", + ), + ( + [ + { + "number": 258, + "baseRefName": "main", + "headRefName": "feat/root", + "baseRefOid": "0" * 39, + "headRefOid": "1" * 40, + } + ], + "baseRefOid", + ), + ], +) +def test_single_pull_request_rejects_malformed_identity( + monkeypatch, + response, +) -> None: + """Malformed API identity never reaches dependency or repair logic.""" + + monkeypatch.setattr(stack, "fetch_pr", lambda _repo, _number: response[0]) + with pytest.raises(RuntimeError, match=response[1]): + stack._single_pull_request("owner/repo", 258) + + def test_stack_dispatches_once_in_declared_dependency_order( monkeypatch, capsys, @@ -114,7 +161,7 @@ def fake_inspect(repo: str, pr: dict, args: argparse.Namespace): monkeypatch.setattr(stack, "inspect_pr", fake_inspect) assert stack.process_stack(arguments((258, 260, 261))) == 0 - assert fetched == [258, 260] + assert fetched == [258, 258, 260] assert inspected == [(258, "main"), (260, "feat/root")] payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) assert payload["autofix_dispatches"] == 1 @@ -155,6 +202,82 @@ def fake_inspect(repo: str, pr: dict, args: argparse.Namespace): assert "expected parent head" in payload["decisions"][-1]["reasons"][0] +def test_stack_refreshes_parent_before_child_validation(monkeypatch, capsys) -> None: + """A parent moving after inspection blocks the child without dispatch.""" + + root = make_pr( + 258, + base_name="main", + base_oid="0" * 40, + head_name="feat/root", + head_oid="1" * 40, + ) + moved_root = {**root, "headRefOid": "4" * 40} + child = make_pr( + 260, + base_name="feat/root", + base_oid="1" * 40, + head_name="feat/child", + head_oid="2" * 40, + ) + fetch_counts = {258: 0, 260: 0} + + def fake_fetch(_repo: str, number: int) -> list[dict]: + fetch_counts[number] += 1 + if number == 258: + return [root if fetch_counts[number] == 1 else moved_root] + return [child] + + inspected: list[int] = [] + monkeypatch.setattr(stack, "fetch_pr", fake_fetch) + monkeypatch.setattr( + stack, + "inspect_pr", + lambda _repo, pr, _args: inspected.append(pr["number"]) + or ("skip", (stack.NO_REPAIR_REASON,)), + ) + + assert stack.process_stack(arguments((258, 260))) == 0 + assert inspected == [258] + assert fetch_counts == {258: 2, 260: 0} + payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) + assert payload["decisions"][-1]["action"] == "wait" + assert "head moved" in payload["decisions"][-1]["reasons"][0] + + +def test_stack_fails_when_parent_refresh_is_invalid(monkeypatch, capsys) -> None: + """An invalid parent refresh blocks the child without inspecting it.""" + + root = make_pr( + 258, + base_name="main", + base_oid="0" * 40, + head_name="feat/root", + head_oid="1" * 40, + ) + fetch_count = 0 + + def fake_fetch(_repo: str, number: int) -> list[dict]: + nonlocal fetch_count + assert number == 258 + fetch_count += 1 + return [root] if fetch_count == 1 else [] + + inspected: list[int] = [] + monkeypatch.setattr(stack, "fetch_pr", fake_fetch) + monkeypatch.setattr( + stack, + "inspect_pr", + lambda _repo, pr, _args: inspected.append(pr["number"]) + or ("skip", (stack.NO_REPAIR_REASON,)), + ) + + assert stack.process_stack(arguments((258, 260))) == 1 + assert inspected == [258] + payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) + assert payload["decisions"][-1]["action"] == "error" + + def test_stack_fails_closed_on_wrong_parent_branch(monkeypatch, capsys) -> None: """A child targeting an unexpected branch is a structural contract failure.""" @@ -323,6 +446,38 @@ def test_stack_handles_inspection_failure_and_empty_queue( assert payload["inspected"] == 0 +@pytest.mark.parametrize( + "decision", + [ + None, + ("unknown", ("reason",)), + ("skip", ()), + ("skip", "reason"), + ("skip", (None,)), + ], +) +def test_stack_rejects_malformed_shared_decisions( + monkeypatch, + capsys, + decision, +) -> None: + """Malformed shared-scheduler outputs become explicit errors.""" + + pr = make_pr( + 258, + base_name="main", + base_oid="0" * 40, + head_name="feat/root", + head_oid="1" * 40, + ) + monkeypatch.setattr(stack, "fetch_pr", lambda _repo, _number: [pr]) + monkeypatch.setattr(stack, "inspect_pr", lambda _repo, _pr, _args: decision) + + assert stack.process_stack(arguments((258,))) == 1 + payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) + assert payload["decisions"][0]["action"] == "error" + + def test_cli_covers_invalid_base_stack_value_and_normal_main(monkeypatch) -> None: """CLI validation reports unsafe values and main delegates normal execution.""" From 89865bc634d901c30e6885bb027ce166e16b34ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 15:23:22 +0900 Subject: [PATCH 10/20] fix(lineageweave): harden hourly review contracts --- ...eageweave-hourly-review-repair-quality.yml | 16 +++++++++-- .../lineageweave-hourly-review-repair.yml | 5 ++-- .../lineageweave-hourly-review-caller.md | 7 +++-- scripts/ci/pr_review_fix_stack_scheduler.py | 6 ++--- .../test_lineageweave_hourly_review_caller.py | 27 ++++++++++++------- tests/test_pr_review_fix_stack_scheduler.py | 10 +++++++ 6 files changed, 53 insertions(+), 18 deletions(-) diff --git a/.github/workflows/lineageweave-hourly-review-repair-quality.yml b/.github/workflows/lineageweave-hourly-review-repair-quality.yml index 6f8fa4f7b..7b2e8d470 100644 --- a/.github/workflows/lineageweave-hourly-review-repair-quality.yml +++ b/.github/workflows/lineageweave-hourly-review-repair-quality.yml @@ -7,24 +7,28 @@ on: - .github/workflows/lineageweave-hourly-review-repair-quality.yml - scripts/ci/pr_review_fix_stack_scheduler.py - tests/test_pr_review_fix_stack_scheduler.py + - tests/test_pr_review_fix_scheduler.py - tests/test_lineageweave_hourly_review_caller.py - docs/doctoring/lineageweave-hourly-review-caller.md - docs/doctoring/lineageweave-buyer-surface-opencode-incident.md + - requirements-opencode-review-ci-hashes.txt push: paths: - .github/workflows/lineageweave-hourly-review-repair.yml - .github/workflows/lineageweave-hourly-review-repair-quality.yml - scripts/ci/pr_review_fix_stack_scheduler.py - tests/test_pr_review_fix_stack_scheduler.py + - tests/test_pr_review_fix_scheduler.py - tests/test_lineageweave_hourly_review_caller.py - docs/doctoring/lineageweave-hourly-review-caller.md - docs/doctoring/lineageweave-buyer-surface-opencode-incident.md + - requirements-opencode-review-ci-hashes.txt permissions: contents: read concurrency: - group: lineageweave-hourly-review-repair-quality-${{ github.event.pull_request.number || github.ref }} + group: lineageweave-hourly-review-quality-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }} cancel-in-progress: true jobs: @@ -41,6 +45,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ github.event.pull_request.head.sha || github.sha }} + fetch-depth: 0 persist-credentials: false - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 @@ -67,4 +72,11 @@ jobs: scripts/ci/pr_review_fix_stack_scheduler.py \ tests/test_lineageweave_hourly_review_caller.py \ tests/test_pr_review_fix_stack_scheduler.py - git diff --check + BASE_SHA="${{ github.event.pull_request.base.sha }}" + HEAD_SHA="${{ github.event.pull_request.head.sha || github.sha }}" + BEFORE_SHA="${{ github.event.before }}" + if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then + git diff --check "$BASE_SHA...$HEAD_SHA" + else + git diff --check "$BEFORE_SHA...$HEAD_SHA" + fi diff --git a/.github/workflows/lineageweave-hourly-review-repair.yml b/.github/workflows/lineageweave-hourly-review-repair.yml index 05746ff1b..c9e86e75c 100644 --- a/.github/workflows/lineageweave-hourly-review-repair.yml +++ b/.github/workflows/lineageweave-hourly-review-repair.yml @@ -70,6 +70,7 @@ jobs: - name: Exchange OpenCode app token for scheduler mutations id: scheduler_app_token + if: ${{ inputs.dry_run != true }} env: OIDC_AUDIENCE: opencode-github-action OPENCODE_API_BASE_URL: https://api.opencode.ai @@ -152,8 +153,8 @@ jobs: - name: Dispatch one dependency-safe review repair env: - GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token }} - MUTATION_CREDENTIAL_AVAILABLE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '' || steps.scheduler_app_token.outputs.available == 'true' }} + GH_TOKEN: ${{ inputs.dry_run == true && github.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token }} + MUTATION_CREDENTIAL_AVAILABLE: ${{ inputs.dry_run == true || secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '' || steps.scheduler_app_token.outputs.available == 'true' }} run: | set -euo pipefail if [ "$MUTATION_CREDENTIAL_AVAILABLE" != "true" ]; then diff --git a/docs/doctoring/lineageweave-hourly-review-caller.md b/docs/doctoring/lineageweave-hourly-review-caller.md index 767485cb4..cd8df17ae 100644 --- a/docs/doctoring/lineageweave-hourly-review-caller.md +++ b/docs/doctoring/lineageweave-hourly-review-caller.md @@ -16,7 +16,7 @@ when exact-head OpenCode feedback, a failed-check root-cause review, or an approved/unreviewed merge-conflict repair is actionable. It never treats a missing initial review as repair evidence. -The caller runs at minute 4 of every hour and also exposes `workflow_dispatch` +The caller runs at minute 4 UTC of every hour and also exposes `workflow_dispatch` for an operator-controlled acceptance run. It does not contain product edit logic, LLM credentials, approval authority, merge authority, or release authority. LineageWeave remains independently deployable; privileged automation @@ -114,6 +114,9 @@ established user token is absent. It accepts only `PR_REVIEW_MERGE_TOKEN`, never uses `secrets: inherit`, receives `NVIDIA_NIM_API_KEY`, or introduces `COPILOT_GITHUB_TOKEN`. +A manual `dry_run` uses the read-only `github.token`, skips app-token exchange, +and never materializes or validates a mutation credential. + Model execution remains inside the separately reviewed central worker. Before protected-main activation, `OPENCODE_REPOSITORY_DISPATCH_TARGETS` must contain the exact `ContextualWisdomLab/LineageWeave` repository. A missing or mismatched @@ -178,7 +181,7 @@ https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-t GitHub, Inc. (n.d.-b). *Reuse workflows*. GitHub Docs. Retrieved August 20, 2026, from -https://docs.github.com/en/actions/how-tos/sharing-automations/reuse-workflows +https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows GitHub, Inc. (n.d.-c). *OpenID Connect reference*. GitHub Docs. Retrieved August 20, 2026, from diff --git a/scripts/ci/pr_review_fix_stack_scheduler.py b/scripts/ci/pr_review_fix_stack_scheduler.py index 8d8483613..ef9965a1b 100644 --- a/scripts/ci/pr_review_fix_stack_scheduler.py +++ b/scripts/ci/pr_review_fix_stack_scheduler.py @@ -179,7 +179,7 @@ def process_stack(args: argparse.Namespace) -> int: if previous is not None and previous_number is not None: try: refreshed_parent = _single_pull_request(args.repo, previous_number) - except RuntimeError as exc: + except (RuntimeError, OSError, ValueError) as exc: action, reasons = "error", (str(exc),) else: if refreshed_parent["headRefOid"].lower() != previous["headRefOid"].lower(): @@ -193,7 +193,7 @@ def process_stack(args: argparse.Namespace) -> int: if not action: try: pr = _single_pull_request(args.repo, number) - except RuntimeError as exc: + except (RuntimeError, OSError, ValueError) as exc: action, reasons = "error", (str(exc),) if not action: expected_name = ( @@ -225,7 +225,7 @@ def process_stack(args: argparse.Namespace) -> int: action, reasons = _normalize_decision( inspect_pr(args.repo, pr, local_args) ) - except (RuntimeError, ValueError) as exc: + except (RuntimeError, OSError, ValueError) as exc: action, reasons = "error", (str(exc),) inspected += 1 diff --git a/tests/test_lineageweave_hourly_review_caller.py b/tests/test_lineageweave_hourly_review_caller.py index 1fb47d498..82669506d 100644 --- a/tests/test_lineageweave_hourly_review_caller.py +++ b/tests/test_lineageweave_hourly_review_caller.py @@ -1,16 +1,16 @@ """Contract tests for LineageWeave's bounded hourly review-repair caller.""" +import re from pathlib import Path -CALLER = Path(".github/workflows/lineageweave-hourly-review-repair.yml") -QUALITY_WORKFLOW = Path( - ".github/workflows/lineageweave-hourly-review-repair-quality.yml" -) -STACK_DRIVER = Path("scripts/ci/pr_review_fix_stack_scheduler.py") -DOCTORING = Path("docs/doctoring/lineageweave-hourly-review-caller.md") -INCIDENT = Path("docs/doctoring/lineageweave-buyer-surface-opencode-incident.md") -ONE_SHOT = Path(".github/workflows/one-shot-repair-lineageweave-stack.yml") +REPO_ROOT = Path(__file__).resolve().parents[1] +CALLER = REPO_ROOT / ".github/workflows/lineageweave-hourly-review-repair.yml" +QUALITY_WORKFLOW = REPO_ROOT / ".github/workflows/lineageweave-hourly-review-repair-quality.yml" +STACK_DRIVER = REPO_ROOT / "scripts/ci/pr_review_fix_stack_scheduler.py" +DOCTORING = REPO_ROOT / "docs/doctoring/lineageweave-hourly-review-caller.md" +INCIDENT = REPO_ROOT / "docs/doctoring/lineageweave-buyer-surface-opencode-incident.md" +ONE_SHOT = REPO_ROOT / ".github/workflows/one-shot-repair-lineageweave-stack.yml" def _read(path: Path) -> str: @@ -50,6 +50,9 @@ def test_lineageweave_caller_is_protected_main_only_and_least_privilege() -> Non assert "PR_REVIEW_MERGE_TOKEN" in caller assert "OPENCODE_APPROVE_TOKEN" in caller assert "persist-credentials: false" in caller + assert "if: ${{ inputs.dry_run != true }}" in caller + assert "github.token" in caller + assert "MUTATION_CREDENTIAL_AVAILABLE: ${{ inputs.dry_run == true ||" in caller assert "--connect-timeout 10" in caller assert "--max-time 30" in caller assert "secrets: inherit" not in caller @@ -71,7 +74,7 @@ def test_stack_driver_is_product_neutral_and_one_shot_is_absent() -> None: driver = _read(STACK_DRIVER) assert "ContextualWisdomLab/LineageWeave" not in driver for number in ("258", "260", "261", "262", "263", "264"): - assert number not in driver + assert re.search(rf"(? None: ".github/workflows/lineageweave-hourly-review-repair-quality.yml", "scripts/ci/pr_review_fix_stack_scheduler.py", "tests/test_pr_review_fix_stack_scheduler.py", + "tests/test_pr_review_fix_scheduler.py", "tests/test_lineageweave_hourly_review_caller.py", "docs/doctoring/lineageweave-hourly-review-caller.md", "docs/doctoring/lineageweave-buyer-surface-opencode-incident.md", + "requirements-opencode-review-ci-hashes.txt", ) assert "\npermissions:\n contents: read\n" in quality assert "cancel-in-progress: true" in quality + assert "lineageweave-hourly-review-quality-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}" in quality assert "persist-credentials: false" in quality + assert "fetch-depth: 0" in quality assert "--require-hashes" in quality assert "tests/test_pr_review_fix_stack_scheduler.py" in quality assert "tests/test_pr_review_fix_scheduler.py" in quality @@ -143,6 +150,8 @@ def test_focused_quality_workflow_tracks_every_owned_contract() -> None: assert "interrogate -vv --fail-under 100" in quality assert "python -m compileall -q \\" in quality assert "git diff --check" in quality + assert 'git diff --check "$BASE_SHA...$HEAD_SHA"' in quality + assert 'git diff --check "$BEFORE_SHA...$HEAD_SHA"' in quality for path in owned_paths: assert quality.count(f" - {path}") == 2 for forbidden in ( diff --git a/tests/test_pr_review_fix_stack_scheduler.py b/tests/test_pr_review_fix_stack_scheduler.py index 047e2670d..058fb96ca 100644 --- a/tests/test_pr_review_fix_stack_scheduler.py +++ b/tests/test_pr_review_fix_stack_scheduler.py @@ -446,6 +446,16 @@ def test_stack_handles_inspection_failure_and_empty_queue( assert payload["inspected"] == 0 +def test_stack_handles_external_os_error(monkeypatch, capsys) -> None: + """An external GitHub transport error fails closed with a visible reason.""" + + monkeypatch.setattr(stack, "fetch_pr", lambda _repo, _number: (_ for _ in ()).throw(OSError("network down"))) + assert stack.process_stack(arguments((258,))) == 1 + payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) + assert payload["decisions"][0]["action"] == "error" + assert payload["decisions"][0]["reasons"] == ["network down"] + + @pytest.mark.parametrize( "decision", [ From 687e1d76ff2914f7706f9b3dd96e8be350431587 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 21:58:36 +0900 Subject: [PATCH 11/20] chore: align LineageWeave repair runner pin --- .github/workflows/lineageweave-hourly-review-repair-quality.yml | 2 +- .github/workflows/lineageweave-hourly-review-repair.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lineageweave-hourly-review-repair-quality.yml b/.github/workflows/lineageweave-hourly-review-repair-quality.yml index 7b2e8d470..29d4b4fad 100644 --- a/.github/workflows/lineageweave-hourly-review-repair-quality.yml +++ b/.github/workflows/lineageweave-hourly-review-repair-quality.yml @@ -38,7 +38,7 @@ jobs: timeout-minutes: 10 steps: - name: Harden runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit - name: Checkout exact source revision diff --git a/.github/workflows/lineageweave-hourly-review-repair.yml b/.github/workflows/lineageweave-hourly-review-repair.yml index c9e86e75c..0c417a74c 100644 --- a/.github/workflows/lineageweave-hourly-review-repair.yml +++ b/.github/workflows/lineageweave-hourly-review-repair.yml @@ -39,7 +39,7 @@ jobs: DRY_RUN: ${{ inputs.dry_run == true }} steps: - name: Harden runner - uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 with: egress-policy: audit From 28bfd155cd168f557de183295c29a79a8cee8150 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 22:31:15 +0900 Subject: [PATCH 12/20] fix(actions): keep LineageWeave caller schedule-only --- .../lineageweave-hourly-review-repair.yml | 16 ++-------------- .../lineageweave-hourly-review-caller.md | 19 ++++++++----------- .../test_lineageweave_hourly_review_caller.py | 10 +++++----- 3 files changed, 15 insertions(+), 30 deletions(-) diff --git a/.github/workflows/lineageweave-hourly-review-repair.yml b/.github/workflows/lineageweave-hourly-review-repair.yml index 0c417a74c..9c8196511 100644 --- a/.github/workflows/lineageweave-hourly-review-repair.yml +++ b/.github/workflows/lineageweave-hourly-review-repair.yml @@ -1,13 +1,6 @@ name: LineageWeave Hourly Review Repair on: - workflow_dispatch: - inputs: - dry_run: - description: Inspect the ordered stack without dispatching a repair - required: false - default: false - type: boolean schedule: # Minute 4 avoids the start-of-hour load peak and existing product callers. - cron: "4 * * * *" @@ -36,7 +29,6 @@ jobs: RETRY_HOURS: "2" AUTOFIX_WORKFLOW: pr-review-autofix.yml AUTOFIX_REPOSITORY: ContextualWisdomLab/.github - DRY_RUN: ${{ inputs.dry_run == true }} steps: - name: Harden runner uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 @@ -70,7 +62,6 @@ jobs: - name: Exchange OpenCode app token for scheduler mutations id: scheduler_app_token - if: ${{ inputs.dry_run != true }} env: OIDC_AUDIENCE: opencode-github-action OPENCODE_API_BASE_URL: https://api.opencode.ai @@ -153,8 +144,8 @@ jobs: - name: Dispatch one dependency-safe review repair env: - GH_TOKEN: ${{ inputs.dry_run == true && github.token || secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token }} - MUTATION_CREDENTIAL_AVAILABLE: ${{ inputs.dry_run == true || secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '' || steps.scheduler_app_token.outputs.available == 'true' }} + GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token }} + MUTATION_CREDENTIAL_AVAILABLE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '' || steps.scheduler_app_token.outputs.available == 'true' }} run: | set -euo pipefail if [ "$MUTATION_CREDENTIAL_AVAILABLE" != "true" ]; then @@ -173,7 +164,4 @@ jobs: --autofix-workflow "$AUTOFIX_WORKFLOW" --autofix-repository "$AUTOFIX_REPOSITORY" ) - if [ "$DRY_RUN" = "true" ]; then - args+=(--dry-run) - fi python3 scripts/ci/pr_review_fix_stack_scheduler.py "${args[@]}" diff --git a/docs/doctoring/lineageweave-hourly-review-caller.md b/docs/doctoring/lineageweave-hourly-review-caller.md index cd8df17ae..349aeefea 100644 --- a/docs/doctoring/lineageweave-hourly-review-caller.md +++ b/docs/doctoring/lineageweave-hourly-review-caller.md @@ -16,11 +16,11 @@ when exact-head OpenCode feedback, a failed-check root-cause review, or an approved/unreviewed merge-conflict repair is actionable. It never treats a missing initial review as repair evidence. -The caller runs at minute 4 UTC of every hour and also exposes `workflow_dispatch` -for an operator-controlled acceptance run. It does not contain product edit -logic, LLM credentials, approval authority, merge authority, or release -authority. LineageWeave remains independently deployable; privileged automation -remains in `ContextualWisdomLab/.github`. +The caller runs at minute 4 UTC of every hour from the protected default branch. +It deliberately exposes no branch-selected `workflow_dispatch` entry point. It +does not contain product edit logic, LLM credentials, approval authority, merge +authority, or release authority. LineageWeave remains independently deployable; +privileged automation remains in `ContextualWisdomLab/.github`. ## Buyer-visible incident @@ -102,8 +102,8 @@ than faster remediation. GitHub scheduled workflows execute from the default branch and can be delayed under Actions load. The cron expression is a heartbeat, not a real-time SLA. -`workflow_dispatch` permits a deliberate post-merge acceptance run without -changing the cadence or credential boundary. +Operators perform post-merge acceptance on the next protected-default-branch +heartbeat so an arbitrary branch can never become the privileged source. ## Credential and model boundary @@ -114,9 +114,6 @@ established user token is absent. It accepts only `PR_REVIEW_MERGE_TOKEN`, never uses `secrets: inherit`, receives `NVIDIA_NIM_API_KEY`, or introduces `COPILOT_GITHUB_TOKEN`. -A manual `dry_run` uses the read-only `github.token`, skips app-token exchange, -and never materializes or validates a mutation credential. - Model execution remains inside the separately reviewed central worker. Before protected-main activation, `OPENCODE_REPOSITORY_DISPATCH_TARGETS` must contain the exact `ContextualWisdomLab/LineageWeave` repository. A missing or mismatched @@ -154,7 +151,7 @@ Machine-checkable contracts require: - explicit ordered queue `258,260,261,262,263,264`; - exact child branch and parent-head validation; - stale-child wait without mutation; -- minute 4 hourly cadence plus a manual acceptance entry point; +- minute 4 hourly cadence without a branch-selected manual entry point; - non-cancelling repository-scoped concurrency; - at most one dispatch and a two-hour same-head retry floor; - read-only workflow contents plus job-scoped OIDC; diff --git a/tests/test_lineageweave_hourly_review_caller.py b/tests/test_lineageweave_hourly_review_caller.py index 82669506d..4a84cda55 100644 --- a/tests/test_lineageweave_hourly_review_caller.py +++ b/tests/test_lineageweave_hourly_review_caller.py @@ -19,12 +19,12 @@ def _read(path: Path) -> str: return path.read_text(encoding="utf-8") -def test_lineageweave_caller_is_manual_hourly_bounded_and_ordered() -> None: +def test_lineageweave_caller_is_hourly_bounded_and_ordered() -> None: """The heartbeat covers all six PRs and permits one dependency-safe repair.""" caller = _read(CALLER) - assert "workflow_dispatch:" in caller + assert "workflow_dispatch:" not in caller assert 'cron: "4 * * * *"' in caller assert "group: lineageweave-hourly-review-repair" in caller assert "cancel-in-progress: false" in caller @@ -50,9 +50,9 @@ def test_lineageweave_caller_is_protected_main_only_and_least_privilege() -> Non assert "PR_REVIEW_MERGE_TOKEN" in caller assert "OPENCODE_APPROVE_TOKEN" in caller assert "persist-credentials: false" in caller - assert "if: ${{ inputs.dry_run != true }}" in caller - assert "github.token" in caller - assert "MUTATION_CREDENTIAL_AVAILABLE: ${{ inputs.dry_run == true ||" in caller + assert "inputs.dry_run" not in caller + assert "github.token" not in caller + assert "MUTATION_CREDENTIAL_AVAILABLE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' ||" in caller assert "--connect-timeout 10" in caller assert "--max-time 30" in caller assert "secrets: inherit" not in caller From 67de5f0ed4ebf193b85af8e665ae0e3a4ceea632 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 22:37:18 +0900 Subject: [PATCH 13/20] fix(actions): avoid duplicate exact-head quality runs --- ...eageweave-hourly-review-repair-quality.yml | 24 ++++--------------- .../lineageweave-hourly-review-caller.md | 4 +++- .../test_lineageweave_hourly_review_caller.py | 6 ++--- 3 files changed, 10 insertions(+), 24 deletions(-) diff --git a/.github/workflows/lineageweave-hourly-review-repair-quality.yml b/.github/workflows/lineageweave-hourly-review-repair-quality.yml index 29d4b4fad..a961973a3 100644 --- a/.github/workflows/lineageweave-hourly-review-repair-quality.yml +++ b/.github/workflows/lineageweave-hourly-review-repair-quality.yml @@ -12,23 +12,12 @@ on: - docs/doctoring/lineageweave-hourly-review-caller.md - docs/doctoring/lineageweave-buyer-surface-opencode-incident.md - requirements-opencode-review-ci-hashes.txt - push: - paths: - - .github/workflows/lineageweave-hourly-review-repair.yml - - .github/workflows/lineageweave-hourly-review-repair-quality.yml - - scripts/ci/pr_review_fix_stack_scheduler.py - - tests/test_pr_review_fix_stack_scheduler.py - - tests/test_pr_review_fix_scheduler.py - - tests/test_lineageweave_hourly_review_caller.py - - docs/doctoring/lineageweave-hourly-review-caller.md - - docs/doctoring/lineageweave-buyer-surface-opencode-incident.md - - requirements-opencode-review-ci-hashes.txt permissions: contents: read concurrency: - group: lineageweave-hourly-review-quality-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }} + group: lineageweave-hourly-review-quality-${{ github.event.pull_request.head.repo.full_name }}-${{ github.event.pull_request.head.ref }} cancel-in-progress: true jobs: @@ -44,7 +33,7 @@ jobs: - name: Checkout exact source revision uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ github.event.pull_request.head.sha || github.sha }} + ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 persist-credentials: false - name: Set up Python @@ -73,10 +62,5 @@ jobs: tests/test_lineageweave_hourly_review_caller.py \ tests/test_pr_review_fix_stack_scheduler.py BASE_SHA="${{ github.event.pull_request.base.sha }}" - HEAD_SHA="${{ github.event.pull_request.head.sha || github.sha }}" - BEFORE_SHA="${{ github.event.before }}" - if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then - git diff --check "$BASE_SHA...$HEAD_SHA" - else - git diff --check "$BEFORE_SHA...$HEAD_SHA" - fi + HEAD_SHA="${{ github.event.pull_request.head.sha }}" + git diff --check "$BASE_SHA...$HEAD_SHA" diff --git a/docs/doctoring/lineageweave-hourly-review-caller.md b/docs/doctoring/lineageweave-hourly-review-caller.md index 349aeefea..5784ae072 100644 --- a/docs/doctoring/lineageweave-hourly-review-caller.md +++ b/docs/doctoring/lineageweave-hourly-review-caller.md @@ -159,7 +159,9 @@ Machine-checkable contracts require: - absence of model, Copilot, merge, release, and target-setting authority; - production statement and branch coverage 100% for the stack driver; - public API docstring coverage 100%; and -- a focused path-filtered quality workflow for caller, driver, tests, and doctoring. +- a focused pull-request-only quality workflow for caller, driver, tests, and + doctoring. A redundant branch-push trigger must not leave a cancelled + duplicate check on the same exact head. Rollback removes only this caller, its focused quality workflow, the product-neutral stack driver if no other caller consumes it, its tests, and its doctoring records. diff --git a/tests/test_lineageweave_hourly_review_caller.py b/tests/test_lineageweave_hourly_review_caller.py index 4a84cda55..c549a0cda 100644 --- a/tests/test_lineageweave_hourly_review_caller.py +++ b/tests/test_lineageweave_hourly_review_caller.py @@ -139,8 +139,9 @@ def test_focused_quality_workflow_tracks_every_owned_contract() -> None: ) assert "\npermissions:\n contents: read\n" in quality + assert "\n push:" not in quality assert "cancel-in-progress: true" in quality - assert "lineageweave-hourly-review-quality-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.event.pull_request.head.ref || github.ref_name }}" in quality + assert "lineageweave-hourly-review-quality-${{ github.event.pull_request.head.repo.full_name }}-${{ github.event.pull_request.head.ref }}" in quality assert "persist-credentials: false" in quality assert "fetch-depth: 0" in quality assert "--require-hashes" in quality @@ -151,9 +152,8 @@ def test_focused_quality_workflow_tracks_every_owned_contract() -> None: assert "python -m compileall -q \\" in quality assert "git diff --check" in quality assert 'git diff --check "$BASE_SHA...$HEAD_SHA"' in quality - assert 'git diff --check "$BEFORE_SHA...$HEAD_SHA"' in quality for path in owned_paths: - assert quality.count(f" - {path}") == 2 + assert quality.count(f" - {path}") == 1 for forbidden in ( "actions: write", "contents: write", From 261bf25b812879f181dfe28c48d83d487cdfa7ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 22:49:16 +0900 Subject: [PATCH 14/20] fix(automation): dispatch reviews for stacked LineageWeave PRs --- ...eageweave-hourly-review-repair-quality.yml | 12 +++++-- .../lineageweave-hourly-review-repair.yml | 22 +++++++++++++ .../lineageweave-hourly-review-caller.md | 17 +++++++--- scripts/ci/pr_review_merge_scheduler.py | 7 ++++ .../test_lineageweave_hourly_review_caller.py | 14 ++++++++ tests/test_pr_review_merge_scheduler.py | 32 +++++++++++++++++++ 6 files changed, 97 insertions(+), 7 deletions(-) diff --git a/.github/workflows/lineageweave-hourly-review-repair-quality.yml b/.github/workflows/lineageweave-hourly-review-repair-quality.yml index a961973a3..6912e5964 100644 --- a/.github/workflows/lineageweave-hourly-review-repair-quality.yml +++ b/.github/workflows/lineageweave-hourly-review-repair-quality.yml @@ -6,8 +6,10 @@ on: - .github/workflows/lineageweave-hourly-review-repair.yml - .github/workflows/lineageweave-hourly-review-repair-quality.yml - scripts/ci/pr_review_fix_stack_scheduler.py + - scripts/ci/pr_review_merge_scheduler.py - tests/test_pr_review_fix_stack_scheduler.py - tests/test_pr_review_fix_scheduler.py + - tests/test_pr_review_merge_scheduler.py - tests/test_lineageweave_hourly_review_caller.py - docs/doctoring/lineageweave-hourly-review-caller.md - docs/doctoring/lineageweave-buyer-surface-opencode-incident.md @@ -51,16 +53,20 @@ jobs: python -m coverage run --branch -m pytest -q \ tests/test_lineageweave_hourly_review_caller.py \ tests/test_pr_review_fix_stack_scheduler.py \ - tests/test_pr_review_fix_scheduler.py + tests/test_pr_review_fix_scheduler.py \ + tests/test_pr_review_merge_scheduler.py python -m coverage report \ --include=scripts/ci/pr_review_fix_stack_scheduler.py \ --fail-under=100 interrogate -vv --fail-under 100 \ - scripts/ci/pr_review_fix_stack_scheduler.py + scripts/ci/pr_review_fix_stack_scheduler.py \ + scripts/ci/pr_review_merge_scheduler.py python -m compileall -q \ scripts/ci/pr_review_fix_stack_scheduler.py \ + scripts/ci/pr_review_merge_scheduler.py \ tests/test_lineageweave_hourly_review_caller.py \ - tests/test_pr_review_fix_stack_scheduler.py + tests/test_pr_review_fix_stack_scheduler.py \ + tests/test_pr_review_merge_scheduler.py BASE_SHA="${{ github.event.pull_request.base.sha }}" HEAD_SHA="${{ github.event.pull_request.head.sha }}" git diff --check "$BASE_SHA...$HEAD_SHA" diff --git a/.github/workflows/lineageweave-hourly-review-repair.yml b/.github/workflows/lineageweave-hourly-review-repair.yml index 9c8196511..4c59fd2f1 100644 --- a/.github/workflows/lineageweave-hourly-review-repair.yml +++ b/.github/workflows/lineageweave-hourly-review-repair.yml @@ -25,6 +25,7 @@ jobs: ROOT_BASE_BRANCH: main PULL_REQUEST_NUMBERS: "258,260,261,262,263,264" MAX_PRS: "6" + OPEN_PR_SCAN_LIMIT: "1000" MAX_DISPATCHES: "1" RETRY_HOURS: "2" AUTOFIX_WORKFLOW: pr-review-autofix.yml @@ -142,6 +143,27 @@ jobs: python3 scripts/ci/pr_review_fix_stack_scheduler.py --self-test git diff --check + - name: Dispatch one missing stacked-PR review + env: + GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token }} + SCHEDULER_ACTIONS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token }} + SCHEDULER_DISPATCH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token }} + SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY: ContextualWisdomLab/.github + run: | + set -euo pipefail + python3 scripts/ci/pr_review_merge_scheduler.py \ + --repo "$TARGET_REPOSITORY" \ + --base-branch "$ROOT_BASE_BRANCH" \ + --project-flow github-flow \ + --max-prs "$OPEN_PR_SCAN_LIMIT" \ + --stacked-only \ + --trigger-reviews \ + --review-dispatch-limit 1 \ + --branch-update-limit 0 \ + --no-enable-auto-merge \ + --merge-mode disabled \ + --no-update-branches + - name: Dispatch one dependency-safe review repair env: GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token }} diff --git a/docs/doctoring/lineageweave-hourly-review-caller.md b/docs/doctoring/lineageweave-hourly-review-caller.md index 5784ae072..65368d622 100644 --- a/docs/doctoring/lineageweave-hourly-review-caller.md +++ b/docs/doctoring/lineageweave-hourly-review-caller.md @@ -10,10 +10,10 @@ stack **#258 → #260 → #261 → #262 → #263 → #264**, passes that explici to a product-neutral ordered-stack repair driver, and dispatches at most one bounded current-head repair per heartbeat. -The initial formal OpenCode generation remains owned by the central mention -router and the organization review/merge scheduler. This caller begins only -when exact-head OpenCode feedback, a failed-check root-cause review, or an -approved/unreviewed merge-conflict repair is actionable. It never treats a +Initial formal OpenCode generation remains owned by the central mention router +and review/merge scheduler. The hourly caller now reuses that same product-neutral +scheduler for one bounded all-open, stacked-PR-only review dispatch before it +inspects the declared repair stack. The repair driver still never treats a missing initial review as repair evidence. The caller runs at minute 4 UTC of every hour from the protected default branch. @@ -105,6 +105,14 @@ under Actions load. The cron expression is a heartbeat, not a real-time SLA. Operators perform post-merge acceptance on the next protected-default-branch heartbeat so an arbitrary branch can never become the privileged source. +The 2026-08-20 production sweep inspected 66 repositories after its single +organization-wide review budget had already been consumed. It reached +LineageWeave later and recorded the current stacked PRs as `OpenCode review +absent`. The dedicated hourly scan therefore spends its one review budget only +on non-draft PRs whose base differs from protected `main`; direct PRs retain the +organization required-workflow path. Review publication remains centralized and +the caller receives no merge authority. + ## Credential and model boundary The caller keeps workflow `GITHUB_TOKEN` at `contents: read`. Its one job receives @@ -149,6 +157,7 @@ Machine-checkable contracts require: - exact `ContextualWisdomLab/LineageWeave` target and protected `main` source; - explicit ordered queue `258,260,261,262,263,264`; +- one all-open stacked-PR review scan with a single dispatch budget; - exact child branch and parent-head validation; - stale-child wait without mutation; - minute 4 hourly cadence without a branch-selected manual entry point; diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index 118d0d903..5b939eb7c 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -3733,6 +3733,11 @@ def parse_args(argv: list[str]) -> argparse.Namespace: default=os.environ.get("MERGE_MODE", "direct_or_auto"), ) parser.add_argument("--update-branches", action=argparse.BooleanOptionalAction, default=True) + parser.add_argument( + "--stacked-only", + action="store_true", + help="Inspect only PRs whose base differs from --base-branch", + ) parser.add_argument("--review-workflow", default="Required OpenCode Review") parser.add_argument("--security-workflow", default="Strix Security Scan") parser.add_argument( @@ -3763,6 +3768,8 @@ def main(argv: list[str]) -> int: if args.branch_update_limit < -1: raise SystemExit("--branch-update-limit must be -1 or greater") prs = fetch_pr(args.repo, args.pr_number) if args.pr_number else fetch_open_prs(args.repo, args.max_prs) + if args.stacked_only: + prs = [pr for pr in prs if pr.get("baseRefName") != args.base_branch] decisions = [] review_dispatches_used = 0 branch_updates_used = 0 diff --git a/tests/test_lineageweave_hourly_review_caller.py b/tests/test_lineageweave_hourly_review_caller.py index c549a0cda..3a9de6db0 100644 --- a/tests/test_lineageweave_hourly_review_caller.py +++ b/tests/test_lineageweave_hourly_review_caller.py @@ -8,6 +8,7 @@ CALLER = REPO_ROOT / ".github/workflows/lineageweave-hourly-review-repair.yml" QUALITY_WORKFLOW = REPO_ROOT / ".github/workflows/lineageweave-hourly-review-repair-quality.yml" STACK_DRIVER = REPO_ROOT / "scripts/ci/pr_review_fix_stack_scheduler.py" +MERGE_DRIVER = REPO_ROOT / "scripts/ci/pr_review_merge_scheduler.py" DOCTORING = REPO_ROOT / "docs/doctoring/lineageweave-hourly-review-caller.md" INCIDENT = REPO_ROOT / "docs/doctoring/lineageweave-buyer-surface-opencode-incident.md" ONE_SHOT = REPO_ROOT / ".github/workflows/one-shot-repair-lineageweave-stack.yml" @@ -33,8 +34,16 @@ def test_lineageweave_caller_is_hourly_bounded_and_ordered() -> None: assert 'PULL_REQUEST_NUMBERS: "258,260,261,262,263,264"' in caller assert 'MAX_PRS: "6"' in caller assert 'MAX_DISPATCHES: "1"' in caller + assert 'OPEN_PR_SCAN_LIMIT: "1000"' in caller assert 'RETRY_HOURS: "2"' in caller assert "pr_review_fix_stack_scheduler.py" in caller + assert "pr_review_merge_scheduler.py" in caller + assert "--stacked-only" in caller + assert "--review-dispatch-limit 1" in caller + assert "--branch-update-limit 0" in caller + assert "--no-enable-auto-merge" in caller + assert "--merge-mode disabled" in caller + assert "--no-update-branches" in caller assert "--pull-request-numbers \"$PULL_REQUEST_NUMBERS\"" in caller @@ -72,7 +81,9 @@ def test_stack_driver_is_product_neutral_and_one_shot_is_absent() -> None: """LineageWeave identity and PR numbers remain in the thin caller only.""" driver = _read(STACK_DRIVER) + merge_driver = _read(MERGE_DRIVER) assert "ContextualWisdomLab/LineageWeave" not in driver + assert "ContextualWisdomLab/LineageWeave" not in merge_driver for number in ("258", "260", "261", "262", "263", "264"): assert re.search(rf"(? None: ".github/workflows/lineageweave-hourly-review-repair.yml", ".github/workflows/lineageweave-hourly-review-repair-quality.yml", "scripts/ci/pr_review_fix_stack_scheduler.py", + "scripts/ci/pr_review_merge_scheduler.py", "tests/test_pr_review_fix_stack_scheduler.py", "tests/test_pr_review_fix_scheduler.py", + "tests/test_pr_review_merge_scheduler.py", "tests/test_lineageweave_hourly_review_caller.py", "docs/doctoring/lineageweave-hourly-review-caller.md", "docs/doctoring/lineageweave-buyer-surface-opencode-incident.md", @@ -147,6 +160,7 @@ def test_focused_quality_workflow_tracks_every_owned_contract() -> None: assert "--require-hashes" in quality assert "tests/test_pr_review_fix_stack_scheduler.py" in quality assert "tests/test_pr_review_fix_scheduler.py" in quality + assert "tests/test_pr_review_merge_scheduler.py" in quality assert "--fail-under=100" in quality assert "interrogate -vv --fail-under 100" in quality assert "python -m compileall -q \\" in quality diff --git a/tests/test_pr_review_merge_scheduler.py b/tests/test_pr_review_merge_scheduler.py index f2dd25813..0ae047ae4 100644 --- a/tests/test_pr_review_merge_scheduler.py +++ b/tests/test_pr_review_merge_scheduler.py @@ -4388,6 +4388,36 @@ def test_main_limits_review_dispatches_and_branch_updates(monkeypatch, capsys): ) +def test_main_stacked_only_excludes_default_branch_pull_requests(monkeypatch): + """A dedicated stack caller spends its review budget only on stacked PRs.""" + + prs = [make_pr(number=1), make_pr(number=2, baseRefName="feature-parent")] + seen = [] + monkeypatch.setattr(sched, "fetch_open_prs", lambda repo, max_prs: prs) + monkeypatch.setattr( + sched, + "inspect_pr", + lambda _repo, pr, **_kwargs: seen.append(pr["number"]) + or sched.Decision(pr["number"], "skip", "done"), + ) + + assert ( + sched.main( + [ + "--repo", + "owner/repo", + "--base-branch", + "main", + "--project-flow", + "github-flow", + "--stacked-only", + ] + ) + == 0 + ) + assert seen == [2] + + def test_main_rejects_invalid_review_dispatch_limit(): with pytest.raises(SystemExit, match="--review-dispatch-limit must be -1 or greater"): sched.main( @@ -4463,6 +4493,7 @@ def fake_split_repo(repo, accepted_invalid=accepted_invalid): "5", "--pr-number", "12", + "--stacked-only", ] ) assert parsed.repo == "owner/repo" @@ -4470,6 +4501,7 @@ def fake_split_repo(repo, accepted_invalid=accepted_invalid): assert parsed.security_workflow == "Strix Security Scan" assert parsed.stale_opencode_minutes == 5 assert parsed.pr_number == 12 + assert parsed.stacked_only assert parsed.merge_mode == "direct_or_auto" assert sched.main(["--self-test"]) == 0 From 5822f84f559abe767ae69f09a7709f968de9305d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 18:56:41 +0900 Subject: [PATCH 15/20] test(stack): cover package import fallback --- .../organization_commercial_readiness_loop.py | 3 +- tests/test_pr_review_fix_stack_scheduler.py | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/scripts/ci/organization_commercial_readiness_loop.py b/scripts/ci/organization_commercial_readiness_loop.py index c00cfa1e0..aa820a503 100644 --- a/scripts/ci/organization_commercial_readiness_loop.py +++ b/scripts/ci/organization_commercial_readiness_loop.py @@ -239,6 +239,7 @@ class GitHubClient: """Use the GitHub CLI as an authenticated, bounded REST transport.""" def __init__(self, token: str, *, timeout_seconds: int = 60) -> None: + """Create a client with an explicit organization-scoped credential.""" if not token: raise GitHubError("GH_TOKEN is required for organization coordination") self._token = token @@ -853,4 +854,4 @@ def main( if __name__ == "__main__": # pragma: no cover - exercised through main() - raise SystemExit(main()) \ No newline at end of file + raise SystemExit(main()) diff --git a/tests/test_pr_review_fix_stack_scheduler.py b/tests/test_pr_review_fix_stack_scheduler.py index 058fb96ca..a06e47394 100644 --- a/tests/test_pr_review_fix_stack_scheduler.py +++ b/tests/test_pr_review_fix_stack_scheduler.py @@ -3,7 +3,9 @@ from __future__ import annotations import argparse +import builtins import json +import runpy import pytest @@ -46,6 +48,35 @@ def arguments(numbers: tuple[int, ...]) -> argparse.Namespace: ) +def test_stack_driver_falls_back_to_package_scheduler(monkeypatch) -> None: + """The stack driver remains importable without its script directory.""" + + real_import = builtins.__import__ + + def import_without_script_directory( + name, + globals_=None, + locals_=None, + fromlist=(), + level=0, + ): + """Reject the flat scheduler import and delegate every other import.""" + + if name == "pr_review_fix_scheduler": + raise ModuleNotFoundError(name) + return real_import(name, globals_, locals_, fromlist, level) + + monkeypatch.setattr(builtins, "__import__", import_without_script_directory) + namespace = runpy.run_path( + "scripts/ci/pr_review_fix_stack_scheduler.py", + run_name="pr_review_fix_stack_scheduler_package_fallback_test", + ) + + loaded = namespace["fetch_pr"] + assert loaded.__name__ == stack.fetch_pr.__name__ + assert loaded.__code__.co_filename == stack.fetch_pr.__code__.co_filename + + def test_parse_pull_request_numbers_preserves_order_and_rejects_ambiguity() -> None: """The explicit queue is positive, unique, bounded, and ordered.""" From 5c80921e2f47434140288b423b7f2195a98c28e8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:11:06 -0700 Subject: [PATCH 16/20] fix(automation): guard LineageWeave review dispatch credentials --- .../lineageweave-hourly-review-repair.yml | 6 +++++ .../test_lineageweave_hourly_review_caller.py | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/.github/workflows/lineageweave-hourly-review-repair.yml b/.github/workflows/lineageweave-hourly-review-repair.yml index 4c59fd2f1..0b33d97d2 100644 --- a/.github/workflows/lineageweave-hourly-review-repair.yml +++ b/.github/workflows/lineageweave-hourly-review-repair.yml @@ -146,11 +146,17 @@ jobs: - name: Dispatch one missing stacked-PR review env: GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token }} + MUTATION_CREDENTIAL_AVAILABLE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' || secrets.OPENCODE_APPROVE_TOKEN != '' || steps.scheduler_app_token.outputs.available == 'true' }} SCHEDULER_ACTIONS_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token }} SCHEDULER_DISPATCH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token }} SCHEDULER_REQUIRED_WORKFLOW_REPOSITORY: ContextualWisdomLab/.github run: | set -euo pipefail + if [ "$MUTATION_CREDENTIAL_AVAILABLE" != "true" ]; then + echo "::error::An established scheduler mutation credential or exchanged OpenCode app token is required." + exit 1 + fi + python3 scripts/ci/pr_review_merge_scheduler.py \ --repo "$TARGET_REPOSITORY" \ --base-branch "$ROOT_BASE_BRANCH" \ diff --git a/tests/test_lineageweave_hourly_review_caller.py b/tests/test_lineageweave_hourly_review_caller.py index 3a9de6db0..acc87b8af 100644 --- a/tests/test_lineageweave_hourly_review_caller.py +++ b/tests/test_lineageweave_hourly_review_caller.py @@ -77,6 +77,30 @@ def test_lineageweave_caller_is_protected_main_only_and_least_privilege() -> Non assert forbidden not in caller +def test_each_mutating_dispatch_fails_closed_without_a_scheduler_credential() -> None: + """Review and repair dispatches emit the same typed credential failure.""" + + caller = _read(CALLER) + availability = ( + "MUTATION_CREDENTIAL_AVAILABLE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' ||" + ) + guard = 'if [ "$MUTATION_CREDENTIAL_AVAILABLE" != "true" ]; then' + diagnostic = ( + "::error::An established scheduler mutation credential or exchanged " + "OpenCode app token is required." + ) + + assert caller.count(availability) == 2 + assert caller.count(guard) == 2 + assert caller.count(diagnostic) == 2 + review_dispatch = caller.split( + " - name: Dispatch one missing stacked-PR review", 1 + )[1].split(" - name: Dispatch one dependency-safe review repair", 1)[0] + assert review_dispatch.index(guard) < review_dispatch.index( + "python3 scripts/ci/pr_review_merge_scheduler.py" + ) + + def test_stack_driver_is_product_neutral_and_one_shot_is_absent() -> None: """LineageWeave identity and PR numbers remain in the thin caller only.""" From f9c18073f9d3ac46826ef61dd18d56d568de58a5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:46:04 -0700 Subject: [PATCH 17/20] fix(scheduler): close stack dispatch race windows --- scripts/ci/pr_review_fix_scheduler.py | 4 +- scripts/ci/pr_review_fix_stack_scheduler.py | 2 + scripts/ci/pr_review_merge_scheduler.py | 2 + tests/test_pr_review_fix_scheduler.py | 31 +++++++++++- tests/test_pr_review_fix_stack_scheduler.py | 54 +++++++++++++++++++++ 5 files changed, 90 insertions(+), 3 deletions(-) mode change 100755 => 100644 scripts/ci/pr_review_fix_scheduler.py diff --git a/scripts/ci/pr_review_fix_scheduler.py b/scripts/ci/pr_review_fix_scheduler.py old mode 100755 new mode 100644 index 2c9745d09..a5de472a8 --- a/scripts/ci/pr_review_fix_scheduler.py +++ b/scripts/ci/pr_review_fix_scheduler.py @@ -207,7 +207,7 @@ def create_fix_marker(repo: str, pr: dict[str, Any], *, dry_run: bool) -> None: [ f"{FIX_MARKER} head_sha={head_sha} epoch={int(time.time())} -->", "", - "Scheduled review-feedback autofix for this PR head.", + "Claimed scheduled review-feedback autofix for this PR head.", "", f"- Head SHA: `{head_sha}`", ] @@ -343,8 +343,8 @@ def inspect_pr( } if repair_mode == "rca": dispatch_kwargs["repair_mode"] = "rca" - dispatch_autofix(repo, pr, **dispatch_kwargs) create_fix_marker(repo, pr, dry_run=args.dry_run) + dispatch_autofix(repo, pr, **dispatch_kwargs) return "dispatch", reasons diff --git a/scripts/ci/pr_review_fix_stack_scheduler.py b/scripts/ci/pr_review_fix_stack_scheduler.py index ef9965a1b..226b0ddd2 100644 --- a/scripts/ci/pr_review_fix_stack_scheduler.py +++ b/scripts/ci/pr_review_fix_stack_scheduler.py @@ -89,6 +89,8 @@ def _single_pull_request(repo: str, number: int) -> dict[str, Any]: for field in ("baseRefOid", "headRefOid"): if not isinstance(record.get(field), str) or not SHA_RE.fullmatch(record[field]): raise RuntimeError(f"pull request response has an invalid {field}") + if record.get("state") != "OPEN": + raise RuntimeError(f"pull request #{number} is not open") return record diff --git a/scripts/ci/pr_review_merge_scheduler.py b/scripts/ci/pr_review_merge_scheduler.py index 641364df1..8ae467452 100644 --- a/scripts/ci/pr_review_merge_scheduler.py +++ b/scripts/ci/pr_review_merge_scheduler.py @@ -24,6 +24,7 @@ fragment SchedulerPullRequestFields on PullRequest { number title + state isDraft mergeable mergeStateStatus @@ -807,6 +808,7 @@ def rest_pr_node(repo: str, pr: dict[str, Any]) -> dict[str, Any]: return { "number": number, "title": pr.get("title"), + "state": str(pr.get("state") or "").upper(), "isDraft": bool(pr.get("draft")), "mergeable": pr.get("mergeable"), "mergeStateStatus": rest_merge_state, diff --git a/tests/test_pr_review_fix_scheduler.py b/tests/test_pr_review_fix_scheduler.py index 74366f686..ba41051c3 100644 --- a/tests/test_pr_review_fix_scheduler.py +++ b/tests/test_pr_review_fix_scheduler.py @@ -156,13 +156,42 @@ def test_process_queue_dispatches_same_repo_current_head(monkeypatch, capsys): assert fix.main(["--repo", "owner/repo", "--base-branch", "main", "--dry-run"]) == 0 assert calls == [ - ("dispatch", "owner/repo", 7, "pr-review-autofix.yml", "ContextualWisdomLab/.github", True, False), ("marker", "owner/repo", 7, True), + ("dispatch", "owner/repo", 7, "pr-review-autofix.yml", "ContextualWisdomLab/.github", True, False), ] payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) assert payload["autofix_dispatches"] == 1 +def test_marker_failure_prevents_untracked_dispatch(monkeypatch) -> None: + """A failed deduplication marker must stop the external dispatch.""" + + pr = make_pr() + dispatched: list[int] = [] + monkeypatch.setattr(fix, "issue_comments", lambda repo, number: []) + monkeypatch.setattr(fix, "needs_autofix", lambda item: (True, ("repair",))) + monkeypatch.setattr( + fix, + "create_fix_marker", + lambda repo, item, dry_run: (_ for _ in ()).throw( + RuntimeError("marker unavailable") + ), + ) + monkeypatch.setattr( + fix, + "dispatch_autofix", + lambda repo, item, **kwargs: dispatched.append(item["number"]), + ) + args = fix.parse_args( + ["--repo", "owner/repo", "--base-branch", "main"] + ) + + with pytest.raises(RuntimeError, match="marker unavailable"): + fix.inspect_pr("owner/repo", pr, args) + + assert dispatched == [] + + def test_autofix_context_filters_outdated_threads_and_renders_checks(): """The context helper filters stale threads and renders compact checks.""" assert context.repo_parts("owner/repo") == ("owner", "repo") diff --git a/tests/test_pr_review_fix_stack_scheduler.py b/tests/test_pr_review_fix_stack_scheduler.py index a06e47394..22abbf7fa 100644 --- a/tests/test_pr_review_fix_stack_scheduler.py +++ b/tests/test_pr_review_fix_stack_scheduler.py @@ -19,6 +19,7 @@ def make_pr( base_oid: str, head_name: str, head_oid: str, + state: str = "OPEN", ) -> dict: """Return one minimal scheduler-shaped pull request.""" @@ -28,6 +29,7 @@ def make_pr( "baseRefOid": base_oid, "headRefName": head_name, "headRefOid": head_oid, + "state": state, } @@ -276,6 +278,58 @@ def fake_fetch(_repo: str, number: int) -> list[dict]: assert "head moved" in payload["decisions"][-1]["reasons"][0] +def test_stack_fails_closed_when_same_head_parent_is_no_longer_open( + monkeypatch, capsys +) -> None: + """A closed same-head parent cannot authorize descendant repair.""" + + root = make_pr( + 258, + base_name="main", + base_oid="0" * 40, + head_name="feat/root", + head_oid="1" * 40, + ) + closed_root = make_pr( + 258, + base_name="main", + base_oid="0" * 40, + head_name="feat/root", + head_oid="1" * 40, + state="CLOSED", + ) + child = make_pr( + 260, + base_name="feat/root", + base_oid="1" * 40, + head_name="feat/child", + head_oid="2" * 40, + ) + fetch_counts = {258: 0, 260: 0} + + def fake_fetch(_repo: str, number: int) -> list[dict]: + fetch_counts[number] += 1 + if number == 258: + return [root if fetch_counts[number] == 1 else closed_root] + return [child] + + inspected: list[int] = [] + monkeypatch.setattr(stack, "fetch_pr", fake_fetch) + monkeypatch.setattr( + stack, + "inspect_pr", + lambda _repo, pr, _args: inspected.append(pr["number"]) + or ("skip", (stack.NO_REPAIR_REASON,)), + ) + + assert stack.process_stack(arguments((258, 260))) == 1 + assert inspected == [258] + assert fetch_counts == {258: 2, 260: 0} + payload = json.loads(capsys.readouterr().out.strip().splitlines()[-1]) + assert payload["decisions"][-1]["action"] == "error" + assert "not open" in payload["decisions"][-1]["reasons"][0] + + def test_stack_fails_when_parent_refresh_is_invalid(monkeypatch, capsys) -> None: """An invalid parent refresh blocks the child without inspecting it.""" From abe5473a7f7da1bb1c87fbb4791e0f2314ee92a9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 03:54:29 -0700 Subject: [PATCH 18/20] test(scheduler): bind focused gate to marker source --- .../workflows/lineageweave-hourly-review-repair-quality.yml | 3 +++ tests/test_lineageweave_hourly_review_caller.py | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/lineageweave-hourly-review-repair-quality.yml b/.github/workflows/lineageweave-hourly-review-repair-quality.yml index 6912e5964..0e2d3cb3a 100644 --- a/.github/workflows/lineageweave-hourly-review-repair-quality.yml +++ b/.github/workflows/lineageweave-hourly-review-repair-quality.yml @@ -5,6 +5,7 @@ on: paths: - .github/workflows/lineageweave-hourly-review-repair.yml - .github/workflows/lineageweave-hourly-review-repair-quality.yml + - scripts/ci/pr_review_fix_scheduler.py - scripts/ci/pr_review_fix_stack_scheduler.py - scripts/ci/pr_review_merge_scheduler.py - tests/test_pr_review_fix_stack_scheduler.py @@ -59,9 +60,11 @@ jobs: --include=scripts/ci/pr_review_fix_stack_scheduler.py \ --fail-under=100 interrogate -vv --fail-under 100 \ + scripts/ci/pr_review_fix_scheduler.py \ scripts/ci/pr_review_fix_stack_scheduler.py \ scripts/ci/pr_review_merge_scheduler.py python -m compileall -q \ + scripts/ci/pr_review_fix_scheduler.py \ scripts/ci/pr_review_fix_stack_scheduler.py \ scripts/ci/pr_review_merge_scheduler.py \ tests/test_lineageweave_hourly_review_caller.py \ diff --git a/tests/test_lineageweave_hourly_review_caller.py b/tests/test_lineageweave_hourly_review_caller.py index acc87b8af..ea5330aac 100644 --- a/tests/test_lineageweave_hourly_review_caller.py +++ b/tests/test_lineageweave_hourly_review_caller.py @@ -164,6 +164,7 @@ def test_focused_quality_workflow_tracks_every_owned_contract() -> None: owned_paths = ( ".github/workflows/lineageweave-hourly-review-repair.yml", ".github/workflows/lineageweave-hourly-review-repair-quality.yml", + "scripts/ci/pr_review_fix_scheduler.py", "scripts/ci/pr_review_fix_stack_scheduler.py", "scripts/ci/pr_review_merge_scheduler.py", "tests/test_pr_review_fix_stack_scheduler.py", From 251c898e2a60df21a23af4dee4e25151cc41ce3d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 02:53:13 +0900 Subject: [PATCH 19/20] fix(ci): refresh secure review gate dependencies --- requirements-pip-audit-ci-hashes.txt | 6 +++--- scripts/ci/test_strix_quick_gate.sh | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements-pip-audit-ci-hashes.txt b/requirements-pip-audit-ci-hashes.txt index ade197a49..0ae099d8f 100644 --- a/requirements-pip-audit-ci-hashes.txt +++ b/requirements-pip-audit-ci-hashes.txt @@ -213,9 +213,9 @@ packaging==26.2 \ # via # pip-audit # pip-requirements-parser -pip==26.1.2 \ - --hash=sha256:382ff9f685ee3bc25864f820aa50505825f10f5458ffff07e30a6d96e5715cab \ - --hash=sha256:f49cd134c61cf2fd75e0ce2676db03e4054504a5a4986d00f8299ae632dc4605 +pip==26.2.1 \ + --hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \ + --hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f # via pip-api pip-api==0.0.34 \ --hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \ diff --git a/scripts/ci/test_strix_quick_gate.sh b/scripts/ci/test_strix_quick_gate.sh index ac9ce1d8b..e2d318287 100755 --- a/scripts/ci/test_strix_quick_gate.sh +++ b/scripts/ci/test_strix_quick_gate.sh @@ -1506,8 +1506,8 @@ assert_pr_review_merge_scheduler_uses_github_actions_bot_token() { assert_file_contains "$workflow_file" "github.event_name == 'pull_request_target' && format('pr-{0}', github.event.pull_request.number)" "scheduler scopes pull_request_target concurrency to the active PR" assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' && github.event.workflow_run.pull_requests[0].number && format('pr-{0}', github.event.workflow_run.pull_requests[0].number)" "scheduler scopes workflow_run concurrency to the completed review PR" assert_file_contains "$workflow_file" "github.event_name == 'schedule' && format('schedule-{0}', github.event.schedule)" "scheduler isolates the 15-minute organization sweep from the separate 30-minute scheduled scan" - assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && github.run_id" "scheduler keeps manual queue scans isolated per run" - assert_file_contains "$workflow_file" "cancel-in-progress: \${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' }}" "scheduler cancels stale PR/review/manual queue scans instead of accumulating merge/update attempts" + assert_file_contains "$workflow_file" "github.event_name == 'repository_dispatch' && format('repo-dispatch-{0}', github.repository)" "scheduler keeps manual queue scans isolated per repository" + assert_file_contains "$workflow_file" "github.event_name == 'workflow_run' && !github.event.workflow_run.pull_requests[0].number" "scheduler cancels only metadata-free workflow-run scans in their isolated fallback group" assert_file_contains "$workflow_file" "timeout-minutes: 60" "organization sweep has enough headroom to finish the complete repository walk" assert_file_contains "$workflow_file" "ORG_SWEEP_TRIGGER_REVIEWS: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps retry missing current-head OpenCode reviews" assert_file_contains "$workflow_file" "ORG_SWEEP_ENABLE_AUTO_MERGE: \${{ github.event_name == 'schedule' ||" "scheduled organization sweeps merge approved current heads" From aeb096a52c5f4c2647f05f54f0aa6b17200a350f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 21:27:22 +0900 Subject: [PATCH 20/20] fix(automation): measure all LineageWeave schedulers --- .../lineageweave-hourly-review-repair-quality.yml | 8 ++------ tests/test_lineageweave_hourly_review_caller.py | 6 ++++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/lineageweave-hourly-review-repair-quality.yml b/.github/workflows/lineageweave-hourly-review-repair-quality.yml index 0e2d3cb3a..ce0837a70 100644 --- a/.github/workflows/lineageweave-hourly-review-repair-quality.yml +++ b/.github/workflows/lineageweave-hourly-review-repair-quality.yml @@ -51,13 +51,9 @@ jobs: run: | set -euo pipefail python -m coverage erase - python -m coverage run --branch -m pytest -q \ - tests/test_lineageweave_hourly_review_caller.py \ - tests/test_pr_review_fix_stack_scheduler.py \ - tests/test_pr_review_fix_scheduler.py \ - tests/test_pr_review_merge_scheduler.py + python -m coverage run --branch -m pytest -q tests python -m coverage report \ - --include=scripts/ci/pr_review_fix_stack_scheduler.py \ + --include=scripts/ci/pr_review_fix_scheduler.py,scripts/ci/pr_review_fix_stack_scheduler.py,scripts/ci/pr_review_merge_scheduler.py \ --fail-under=100 interrogate -vv --fail-under 100 \ scripts/ci/pr_review_fix_scheduler.py \ diff --git a/tests/test_lineageweave_hourly_review_caller.py b/tests/test_lineageweave_hourly_review_caller.py index ea5330aac..98f596bed 100644 --- a/tests/test_lineageweave_hourly_review_caller.py +++ b/tests/test_lineageweave_hourly_review_caller.py @@ -186,7 +186,13 @@ def test_focused_quality_workflow_tracks_every_owned_contract() -> None: assert "tests/test_pr_review_fix_stack_scheduler.py" in quality assert "tests/test_pr_review_fix_scheduler.py" in quality assert "tests/test_pr_review_merge_scheduler.py" in quality + assert "python -m coverage run --branch -m pytest -q tests" in quality assert "--fail-under=100" in quality + coverage_report = quality.split("python -m coverage report", 1)[1].split( + "--fail-under=100", 1 + )[0] + for driver in ("fix_scheduler", "fix_stack_scheduler", "merge_scheduler"): + assert f"scripts/ci/pr_review_{driver}.py" in coverage_report assert "interrogate -vv --fail-under 100" in quality assert "python -m compileall -q \\" in quality assert "git diff --check" in quality