From 7464562cc5022b15bcca423a8d106484845c80fc Mon Sep 17 00:00:00 2001 From: seonghobae Date: Tue, 25 Aug 2026 20:01:30 +0900 Subject: [PATCH 01/20] ci(ops): add hourly trusted PR and gap loop --- .github/workflows/hourly-pr-gap-loop.yml | 75 +++++++++++ scripts/ops/gap_baseline_freshness.py | 153 +++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 .github/workflows/hourly-pr-gap-loop.yml create mode 100644 scripts/ops/gap_baseline_freshness.py diff --git a/.github/workflows/hourly-pr-gap-loop.yml b/.github/workflows/hourly-pr-gap-loop.yml new file mode 100644 index 000000000..69bc269b2 --- /dev/null +++ b/.github/workflows/hourly-pr-gap-loop.yml @@ -0,0 +1,75 @@ +name: Hourly PR and Gap Loop + +# Durable execution-loop driver for Orgmetra. +# +# Contract (docs/product-technical-gap-baseline.md "Execution loop"): +# each hour this workflow (1) re-runs the TRUSTED central review/merge +# scheduler so approval-ready heads keep integrating even when no PR event +# fires, and (2) audits docs/product-technical-gap-baseline.md against live +# repository truth so the next development iteration starts from fresh state. +# +# All mutations stay inside the sanctioned central machinery; this workflow +# never bypasses ruleset 18156473 gates and never self-approves. + +on: + schedule: + # Hourly, offset from quarter-hour org sweeps to avoid provider contention. + - cron: "37 * * * *" + workflow_dispatch: + +permissions: + contents: read + pull-requests: read + issues: read + checks: read + statuses: read + +concurrency: + group: hourly-pr-gap-loop + cancel-in-progress: false + +jobs: + scheduler-sweep: + name: Trusted review and merge sweep + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + # Review dispatch stays bounded (2 per hour) because the central org + # queue sweep additionally dispatches reviews every 15 minutes; the + # bounded budget prevents model-provider saturation while guaranteeing + # progress for lanes whose last event predates their readiness. + - name: Invoke central PR review and merge scheduler + uses: ContextualWisdomLab/.github/.github/workflows/pr-review-merge-scheduler.yml@main + with: + dry_run: "false" + max_prs: "100" + trigger_reviews: true + review_dispatch_limit: "2" + branch_update_limit: "2" + enable_auto_merge: true + merge_mode: direct_or_auto + update_branches: true + stale_opencode_minutes: "90" + project_flow: git-flow + base_branch: develop + secrets: inherit + + gap-baseline-freshness: + name: Gap baseline freshness audit + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout default branch truth + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: Set up Python + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.13" + - name: Audit gap baseline against live repository state + env: + GH_TOKEN: ${{ github.token }} + run: | + python3 scripts/ops/gap_baseline_freshness.py \ + --baseline docs/product-technical-gap-baseline.md >> "$GITHUB_STEP_SUMMARY" diff --git a/scripts/ops/gap_baseline_freshness.py b/scripts/ops/gap_baseline_freshness.py new file mode 100644 index 000000000..5b3c22ebb --- /dev/null +++ b/scripts/ops/gap_baseline_freshness.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Audit the product-technical gap baseline against live repository truth. + +This script is the operational regression required by the baseline document's +own execution-loop contract: every loop must refetch live GitHub state and +reject stale buyer copy before acting. It never hard-codes volatile payloads; +every comparison is computed at runtime from the recorded inventory date, the +live default branch, and the live open pull-request/issue queues. + +Exit codes: + 0 audit completed; findings are reported in the step summary text + 2 contract violation (unreadable/invalid baseline structure) + +The output is plain Markdown so callers can append it directly to +``$GITHUB_STEP_SUMMARY``. +""" + +from __future__ import annotations + +import argparse +import json +import re +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path + +INVENTORY_DATE_PATTERN = re.compile( + r"^Inventory date:\s*(\d{4}-\d{2}-\d{2})", re.MULTILINE +) + + +def _run_gh_json(arguments: list[str]) -> list[dict[str, object]]: + """Return parsed JSON from one ``gh api`` invocation as a list payload.""" + completed = subprocess.run( + ["gh", *arguments], + check=False, + capture_output=True, + text=True, + ) + if completed.returncode != 0: + raise RuntimeError(f"gh {' '.join(arguments)} failed: {completed.stderr.strip()}") + return json.loads(completed.stdout) + + +def _live_state() -> dict[str, object]: + """Fetch the live open PR/issue queue and newest develop integration.""" + open_pull_requests = _run_gh_json( + [ + "api", + "repos/{owner}/{repo}/pulls?state=open&per_page=100".replace( + "{owner}/{repo}", "ContextualWisdomLab/Orgmetra" + ), + ] + ) + if not isinstance(open_pull_requests, list): + raise RuntimeError("unexpected pull request payload shape") + open_issues = _run_gh_json( + ["api", "repos/ContextualWisdomLab/Orgmetra/issues?state=open&per_page=100"] + ) + if not isinstance(open_issues, list): + raise RuntimeError("unexpected issue payload shape") + # Issues and PRs share the issues endpoint; keep only genuine issues. + genuine_issues = [item for item in open_issues if "pull_request" not in item] + commits = _run_gh_json( + ["api", "repos/ContextualWisdomLab/Orgmetra/commits?sha=develop&per_page=1"] + ) + newest_commit_date = None + if isinstance(commits, list) and commits: + commit_date = commits[0]["commit"]["committer"]["date"] + newest_commit_date = str(commit_date) + return { + "open_pull_requests": len(open_pull_requests), + "open_issues": len(genuine_issues), + "newest_develop_commit_date": newest_commit_date, + } + + +def main() -> int: + """Print a Markdown freshness report for the gap baseline snapshot.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--baseline", default="docs/product-technical-gap-baseline.md") + arguments = parser.parse_args() + + baseline_path = Path(arguments.baseline) + try: + baseline_text = baseline_path.read_text(encoding="utf-8") + except OSError as error: + print(f"gap-baseline freshness: FAIL unreadable baseline: {error}") + return 2 + + match = INVENTORY_DATE_PATTERN.search(baseline_text) + if match is None: + print("gap-baseline freshness: FAIL missing 'Inventory date:' header") + return 2 + + inventory_date = datetime.strptime(match.group(1), "%Y-%m-%d").replace( + tzinfo=timezone.utc + ) + now = datetime.now(tz=timezone.utc) + age_days = (now - inventory_date).total_seconds() / 86400 + + lines = [ + "## Gap baseline freshness", + "", + f"- Baseline file: `{baseline_path.as_posix()}`", + f"- Inventory date: {inventory_date.date().isoformat()} " + f"(age {age_days:.1f} days)", + ] + + try: + state = _live_state() + except (RuntimeError, ValueError, KeyError) as error: + lines.append(f"- Live-state fetch failed (transient?): {error}") + print("\n".join(lines)) + return 0 + + lines.extend( + [ + f"- Live open pull requests: {state['open_pull_requests']}", + f"- Live open issues: {state['open_issues']}", + "- Newest develop integration: " + f"{state['newest_develop_commit_date']}", + ] + ) + + newest_integration = state.get("newest_develop_commit_date") + integrations_after_snapshot = False + if isinstance(newest_integration, str): + integration_time = datetime.fromisoformat( + newest_integration.replace("Z", "+00:00") + ) + integrations_after_snapshot = integration_time > inventory_date + + if integrations_after_snapshot: + lines.append( + "- Result: **refresh candidate** — `develop` integrated commits after " + "the recorded inventory date. Per the execution loop, refresh the " + "baseline only when buyer/product-visible truth changed; otherwise " + "record this audit as observed." + ) + else: + lines.append( + "- Result: current — no develop integration is newer than the " + "recorded inventory snapshot." + ) + + print("\n".join(lines)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From b68fc246c01ea4362920cba57bcc063e7fd65945 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 04:12:12 -0700 Subject: [PATCH 02/20] test(ops): require pinned reusable workflow and full queue pagination --- tests/test_hourly_pr_gap_loop.py | 89 ++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 tests/test_hourly_pr_gap_loop.py diff --git a/tests/test_hourly_pr_gap_loop.py b/tests/test_hourly_pr_gap_loop.py new file mode 100644 index 000000000..56552b61e --- /dev/null +++ b/tests/test_hourly_pr_gap_loop.py @@ -0,0 +1,89 @@ +"""Regression tests for the scheduled Orgmetra PR/gap loop.""" + +from __future__ import annotations + +import importlib.util +import json +import re +from pathlib import Path +from types import ModuleType + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / ".github" / "workflows" / "hourly-pr-gap-loop.yml" +SCRIPT = ROOT / "scripts" / "ops" / "gap_baseline_freshness.py" +CENTRAL_SCHEDULER = ( + "ContextualWisdomLab/.github/.github/workflows/" + "pr-review-merge-scheduler.yml" +) + + +def _load_script() -> ModuleType: + spec = importlib.util.spec_from_file_location("gap_baseline_freshness", SCRIPT) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_reusable_scheduler_is_job_level_and_immutably_pinned() -> None: + """The scheduled mutation driver must call one pinned reusable workflow job.""" + text = WORKFLOW.read_text(encoding="utf-8") + scheduler_block = text.split(" scheduler-sweep:\n", 1)[1].split( + "\n gap-baseline-freshness:", 1 + )[0] + + assert "\n steps:" not in scheduler_block + assert "@main" not in scheduler_block + assert re.search( + rf"^ uses: {re.escape(CENTRAL_SCHEDULER)}@[0-9a-f]{{40}}$", + scheduler_block, + re.MULTILINE, + ) + assert " secrets: inherit" in scheduler_block + + +def test_live_queue_counts_request_all_pages(monkeypatch) -> None: + """Open PR/issue counts must not silently truncate at the first 100 rows.""" + module = _load_script() + calls: list[list[str]] = [] + + class Completed: + returncode = 0 + stderr = "" + + def __init__(self, stdout: str) -> None: + self.stdout = stdout + + def fake_run(arguments: list[str], **_kwargs: object) -> Completed: + calls.append(arguments) + joined = " ".join(arguments) + if "/pulls?" in joined: + return Completed(json.dumps([[{"number": 1}], [{"number": 2}]])) + if "/issues?" in joined: + return Completed( + json.dumps([[{"number": 3}], [{"number": 4, "pull_request": {}}]]) + ) + if "/commits?" in joined: + return Completed( + json.dumps( + [ + { + "commit": { + "committer": {"date": "2026-08-25T00:00:00Z"} + } + } + ] + ) + ) + raise AssertionError(f"unexpected gh invocation: {arguments}") + + monkeypatch.setattr(module.subprocess, "run", fake_run) + state = module._live_state() + + assert state["open_pull_requests"] == 2 + assert state["open_issues"] == 1 + queue_calls = [call for call in calls if "/pulls?" in " ".join(call) or "/issues?" in " ".join(call)] + assert len(queue_calls) == 2 + for call in queue_calls: + assert "--paginate" in call + assert "--slurp" in call From d689b999f33f7cd189c04d7015f2d05f14c2ec6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 04:12:45 -0700 Subject: [PATCH 03/20] fix(ops): call central scheduler as pinned reusable job --- .github/workflows/hourly-pr-gap-loop.yml | 58 ++++++++++++------------ 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/.github/workflows/hourly-pr-gap-loop.yml b/.github/workflows/hourly-pr-gap-loop.yml index 69bc269b2..58d8b722a 100644 --- a/.github/workflows/hourly-pr-gap-loop.yml +++ b/.github/workflows/hourly-pr-gap-loop.yml @@ -8,8 +8,9 @@ name: Hourly PR and Gap Loop # fires, and (2) audits docs/product-technical-gap-baseline.md against live # repository truth so the next development iteration starts from fresh state. # -# All mutations stay inside the sanctioned central machinery; this workflow -# never bypasses ruleset 18156473 gates and never self-approves. +# The central scheduler is called only through an immutable reviewed commit. +# Its mutation permissions are scoped to that reusable-workflow job; the local +# freshness audit remains read-only and never self-approves or bypasses rules. on: schedule: @@ -17,12 +18,7 @@ on: - cron: "37 * * * *" workflow_dispatch: -permissions: - contents: read - pull-requests: read - issues: read - checks: read - statuses: read +permissions: {} concurrency: group: hourly-pr-gap-loop @@ -31,33 +27,35 @@ concurrency: jobs: scheduler-sweep: name: Trusted review and merge sweep - runs-on: ubuntu-latest - timeout-minutes: 30 - steps: - # Review dispatch stays bounded (2 per hour) because the central org - # queue sweep additionally dispatches reviews every 15 minutes; the - # bounded budget prevents model-provider saturation while guaranteeing - # progress for lanes whose last event predates their readiness. - - name: Invoke central PR review and merge scheduler - uses: ContextualWisdomLab/.github/.github/workflows/pr-review-merge-scheduler.yml@main - with: - dry_run: "false" - max_prs: "100" - trigger_reviews: true - review_dispatch_limit: "2" - branch_update_limit: "2" - enable_auto_merge: true - merge_mode: direct_or_auto - update_branches: true - stale_opencode_minutes: "90" - project_flow: git-flow - base_branch: develop - secrets: inherit + uses: ContextualWisdomLab/.github/.github/workflows/pr-review-merge-scheduler.yml@8fd471a31399a914d9cb22a840f4a4c68e010ea6 + permissions: + actions: write + checks: read + contents: write + id-token: write + pull-requests: write + with: + dry_run: false + max_prs: "100" + trigger_reviews: true + review_dispatch_limit: "2" + branch_update_limit: "2" + enable_auto_merge: true + merge_mode: direct_or_auto + update_branches: true + stale_opencode_minutes: "90" + project_flow: git-flow + base_branch: develop + secrets: inherit gap-baseline-freshness: name: Gap baseline freshness audit runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + contents: read + pull-requests: read + issues: read steps: - name: Checkout default branch truth uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 From f0e80a92bd84bc7052e2967b93e8229b7340224b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 04:13:11 -0700 Subject: [PATCH 04/20] fix(ops): paginate complete live queue truth --- scripts/ops/gap_baseline_freshness.py | 70 +++++++++++++++++++-------- 1 file changed, 49 insertions(+), 21 deletions(-) diff --git a/scripts/ops/gap_baseline_freshness.py b/scripts/ops/gap_baseline_freshness.py index 5b3c22ebb..160df720c 100644 --- a/scripts/ops/gap_baseline_freshness.py +++ b/scripts/ops/gap_baseline_freshness.py @@ -5,7 +5,7 @@ own execution-loop contract: every loop must refetch live GitHub state and reject stale buyer copy before acting. It never hard-codes volatile payloads; every comparison is computed at runtime from the recorded inventory date, the -live default branch, and the live open pull-request/issue queues. +live default branch, and the complete live open pull-request/issue queues. Exit codes: 0 audit completed; findings are reported in the step summary text @@ -19,6 +19,7 @@ import argparse import json +import os import re import subprocess import sys @@ -28,47 +29,74 @@ INVENTORY_DATE_PATTERN = re.compile( r"^Inventory date:\s*(\d{4}-\d{2}-\d{2})", re.MULTILINE ) +REPOSITORY_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") -def _run_gh_json(arguments: list[str]) -> list[dict[str, object]]: - """Return parsed JSON from one ``gh api`` invocation as a list payload.""" +def _run_gh_json( + arguments: list[str], *, paginate: bool = False +) -> list[dict[str, object]]: + """Return a list payload from ``gh api``, flattening all pages when asked.""" + command = ["gh", *arguments] + if paginate: + command.extend(["--paginate", "--slurp"]) completed = subprocess.run( - ["gh", *arguments], + command, check=False, capture_output=True, text=True, ) if completed.returncode != 0: raise RuntimeError(f"gh {' '.join(arguments)} failed: {completed.stderr.strip()}") - return json.loads(completed.stdout) + + payload = json.loads(completed.stdout) + if paginate: + if not isinstance(payload, list): + raise RuntimeError("unexpected paginated GitHub payload shape") + flattened: list[dict[str, object]] = [] + for page in payload: + if not isinstance(page, list) or not all(isinstance(item, dict) for item in page): + raise RuntimeError("unexpected paginated GitHub page shape") + flattened.extend(page) + return flattened + + if not isinstance(payload, list) or not all(isinstance(item, dict) for item in payload): + raise RuntimeError("unexpected GitHub list payload shape") + return payload + + +def _live_repository() -> str: + """Return the repository this workflow actually executes in.""" + repository = os.environ.get("GITHUB_REPOSITORY", "ContextualWisdomLab/Orgmetra") + if REPOSITORY_PATTERN.fullmatch(repository) is None: + raise RuntimeError("invalid GITHUB_REPOSITORY shape") + return repository def _live_state() -> dict[str, object]: - """Fetch the live open PR/issue queue and newest develop integration.""" + """Fetch complete live open PR/issue queues and newest develop integration.""" + repository = _live_repository() open_pull_requests = _run_gh_json( - [ - "api", - "repos/{owner}/{repo}/pulls?state=open&per_page=100".replace( - "{owner}/{repo}", "ContextualWisdomLab/Orgmetra" - ), - ] + ["api", f"repos/{repository}/pulls?state=open&per_page=100"], + paginate=True, ) - if not isinstance(open_pull_requests, list): - raise RuntimeError("unexpected pull request payload shape") open_issues = _run_gh_json( - ["api", "repos/ContextualWisdomLab/Orgmetra/issues?state=open&per_page=100"] + ["api", f"repos/{repository}/issues?state=open&per_page=100"], + paginate=True, ) - if not isinstance(open_issues, list): - raise RuntimeError("unexpected issue payload shape") # Issues and PRs share the issues endpoint; keep only genuine issues. genuine_issues = [item for item in open_issues if "pull_request" not in item] commits = _run_gh_json( - ["api", "repos/ContextualWisdomLab/Orgmetra/commits?sha=develop&per_page=1"] + ["api", f"repos/{repository}/commits?sha=develop&per_page=1"] ) newest_commit_date = None - if isinstance(commits, list) and commits: - commit_date = commits[0]["commit"]["committer"]["date"] - newest_commit_date = str(commit_date) + if commits: + commit = commits[0].get("commit") + if not isinstance(commit, dict): + raise RuntimeError("unexpected commit payload shape") + committer = commit.get("committer") + if not isinstance(committer, dict) or not isinstance(committer.get("date"), str): + raise RuntimeError("unexpected commit committer payload shape") + newest_commit_date = committer["date"] return { "open_pull_requests": len(open_pull_requests), "open_issues": len(genuine_issues), From 97095e199bf4f531aab277acb7e64577af11fb6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 04:14:40 -0700 Subject: [PATCH 05/20] test(ops): keep pre-baseline hourly audit nonfatal --- tests/test_hourly_pr_gap_loop.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/test_hourly_pr_gap_loop.py b/tests/test_hourly_pr_gap_loop.py index 56552b61e..5e19efbc6 100644 --- a/tests/test_hourly_pr_gap_loop.py +++ b/tests/test_hourly_pr_gap_loop.py @@ -82,8 +82,24 @@ def fake_run(arguments: list[str], **_kwargs: object) -> Completed: assert state["open_pull_requests"] == 2 assert state["open_issues"] == 1 - queue_calls = [call for call in calls if "/pulls?" in " ".join(call) or "/issues?" in " ".join(call)] + queue_calls = [ + call + for call in calls + if "/pulls?" in " ".join(call) or "/issues?" in " ".join(call) + ] assert len(queue_calls) == 2 for call in queue_calls: assert "--paginate" in call assert "--slurp" in call + + +def test_missing_baseline_is_nonfatal_until_dependency_integrates( + monkeypatch, tmp_path, capsys +) -> None: + """A pre-#100 scheduled run must report absence without staying permanently red.""" + module = _load_script() + missing = tmp_path / "product-technical-gap-baseline.md" + monkeypatch.setattr(module.sys, "argv", ["gap-baseline-freshness", "--baseline", str(missing)]) + + assert module.main() == 0 + assert "baseline not present" in capsys.readouterr().out.lower() From e22aed521eb309e8fb7215f15c3eacfd15fd2e66 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 04:15:05 -0700 Subject: [PATCH 06/20] fix(ops): keep pre-baseline audit nonfatal --- scripts/ops/gap_baseline_freshness.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/scripts/ops/gap_baseline_freshness.py b/scripts/ops/gap_baseline_freshness.py index 160df720c..b64a69f30 100644 --- a/scripts/ops/gap_baseline_freshness.py +++ b/scripts/ops/gap_baseline_freshness.py @@ -8,8 +8,8 @@ live default branch, and the complete live open pull-request/issue queues. Exit codes: - 0 audit completed; findings are reported in the step summary text - 2 contract violation (unreadable/invalid baseline structure) + 0 audit completed, or the baseline has not integrated yet; findings are reported + 2 contract violation (an existing baseline is unreadable/invalid) The output is plain Markdown so callers can append it directly to ``$GITHUB_STEP_SUMMARY``. @@ -113,6 +113,13 @@ def main() -> int: baseline_path = Path(arguments.baseline) try: baseline_text = baseline_path.read_text(encoding="utf-8") + except FileNotFoundError: + print( + "gap-baseline freshness: baseline not present on this integrated " + "branch yet; audit remains non-mutating and will activate when the " + "baseline owner integrates" + ) + return 0 except OSError as error: print(f"gap-baseline freshness: FAIL unreadable baseline: {error}") return 2 From 845f15045087414d1dac20e0e6fea746f5c4b2b0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 04:17:41 -0700 Subject: [PATCH 07/20] test(ops): forbid forwarding independent-review secrets --- tests/test_hourly_pr_gap_loop.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/test_hourly_pr_gap_loop.py b/tests/test_hourly_pr_gap_loop.py index 5e19efbc6..e29e00d80 100644 --- a/tests/test_hourly_pr_gap_loop.py +++ b/tests/test_hourly_pr_gap_loop.py @@ -25,8 +25,8 @@ def _load_script() -> ModuleType: return module -def test_reusable_scheduler_is_job_level_and_immutably_pinned() -> None: - """The scheduled mutation driver must call one pinned reusable workflow job.""" +def test_reusable_scheduler_is_job_level_immutably_pinned_and_least_privileged() -> None: + """Call the pinned scheduler as a job without forwarding reviewer credentials.""" text = WORKFLOW.read_text(encoding="utf-8") scheduler_block = text.split(" scheduler-sweep:\n", 1)[1].split( "\n gap-baseline-freshness:", 1 @@ -39,7 +39,9 @@ def test_reusable_scheduler_is_job_level_and_immutably_pinned() -> None: scheduler_block, re.MULTILINE, ) - assert " secrets: inherit" in scheduler_block + # The central owner already supports OIDC app-token exchange. The caller + # must not forward repository/org secrets such as independent-review tokens. + assert "secrets: inherit" not in scheduler_block def test_live_queue_counts_request_all_pages(monkeypatch) -> None: @@ -99,7 +101,11 @@ def test_missing_baseline_is_nonfatal_until_dependency_integrates( """A pre-#100 scheduled run must report absence without staying permanently red.""" module = _load_script() missing = tmp_path / "product-technical-gap-baseline.md" - monkeypatch.setattr(module.sys, "argv", ["gap-baseline-freshness", "--baseline", str(missing)]) + monkeypatch.setattr( + module.sys, + "argv", + ["gap-baseline-freshness", "--baseline", str(missing)], + ) assert module.main() == 0 assert "baseline not present" in capsys.readouterr().out.lower() From 494be78dae35f91c690b52fd14e5355facb5403e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 04:17:58 -0700 Subject: [PATCH 08/20] fix(ops): keep reviewer credentials outside scheduler caller --- .github/workflows/hourly-pr-gap-loop.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/hourly-pr-gap-loop.yml b/.github/workflows/hourly-pr-gap-loop.yml index 58d8b722a..71f7f6c0d 100644 --- a/.github/workflows/hourly-pr-gap-loop.yml +++ b/.github/workflows/hourly-pr-gap-loop.yml @@ -11,6 +11,9 @@ name: Hourly PR and Gap Loop # The central scheduler is called only through an immutable reviewed commit. # Its mutation permissions are scoped to that reusable-workflow job; the local # freshness audit remains read-only and never self-approves or bypasses rules. +# Repository/organization secrets are deliberately not inherited: the central +# owner already supports OIDC app-token exchange, so independent-review or PAT +# credentials do not cross this caller boundary. on: schedule: @@ -46,7 +49,6 @@ jobs: stale_opencode_minutes: "90" project_flow: git-flow base_branch: develop - secrets: inherit gap-baseline-freshness: name: Gap baseline freshness audit From 829bccad10c22ba7f118114b359a4406be991abf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 04:19:05 -0700 Subject: [PATCH 09/20] docs(ops): record primary reusable-workflow security references --- .../doctoring/hourly-pr-gap-loop-references.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 docs/doctoring/hourly-pr-gap-loop-references.md diff --git a/docs/doctoring/hourly-pr-gap-loop-references.md b/docs/doctoring/hourly-pr-gap-loop-references.md new file mode 100644 index 000000000..60d2820d4 --- /dev/null +++ b/docs/doctoring/hourly-pr-gap-loop-references.md @@ -0,0 +1,18 @@ +# Hourly PR/gap-loop primary references + +Verified against GitHub's current official documentation on **2026-08-25**. These sources define the syntax and trust-boundary assumptions used by the active Orgmetra hourly PR/gap-loop PR; they do not replace the central `.github` repository's published scheduler contract or Orgmetra's effective ruleset. + +## APA 7 references + +GitHub. (n.d.). *Reuse workflows*. GitHub Docs. Retrieved August 25, 2026, from https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows + +GitHub. (n.d.). *Reusing workflow configurations*. GitHub Docs. Retrieved August 25, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations + +GitHub. (n.d.). *Workflow syntax for GitHub Actions*. GitHub Docs. Retrieved August 25, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax + +## Decision relevance + +- GitHub documents reusable workflows as job-level `jobs..uses` calls, not step-level actions. The supported calling-job keywords include `uses`, `with`, `secrets`, `permissions`, `needs`, `if`, `strategy`, and `concurrency`. +- GitHub documents a full commit SHA as the safest reusable-workflow reference for stability and security. Orgmetra therefore pins the read-only central owner contract to one freshly verified commit instead of following mutable `@main` at execution time. +- GitHub documents `secrets: inherit` as forwarding all secrets available to the caller into the directly called workflow. Because the central scheduler already supports OIDC app-token exchange, the Orgmetra caller deliberately does **not** inherit repository/organization secrets; this keeps independent-review credentials outside the caller boundary. +- The central scheduler remains the owner of review dispatch and protected mutations. Orgmetra only supplies bounded inputs and job permissions required by that published reusable-workflow contract. From c0d1465f7b35317fc22f239ca8de0c9b736ebc94 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 04:20:59 -0700 Subject: [PATCH 10/20] test(ops): forbid a second scheduled PR writer --- tests/test_hourly_pr_gap_loop.py | 34 +++++++++++--------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/tests/test_hourly_pr_gap_loop.py b/tests/test_hourly_pr_gap_loop.py index e29e00d80..047b80499 100644 --- a/tests/test_hourly_pr_gap_loop.py +++ b/tests/test_hourly_pr_gap_loop.py @@ -1,20 +1,16 @@ -"""Regression tests for the scheduled Orgmetra PR/gap loop.""" +"""Regression tests for the scheduled Orgmetra gap-baseline audit.""" from __future__ import annotations import importlib.util import json -import re from pathlib import Path from types import ModuleType ROOT = Path(__file__).resolve().parents[1] WORKFLOW = ROOT / ".github" / "workflows" / "hourly-pr-gap-loop.yml" SCRIPT = ROOT / "scripts" / "ops" / "gap_baseline_freshness.py" -CENTRAL_SCHEDULER = ( - "ContextualWisdomLab/.github/.github/workflows/" - "pr-review-merge-scheduler.yml" -) +CENTRAL_SCHEDULER = "pr-review-merge-scheduler.yml" def _load_script() -> ModuleType: @@ -25,23 +21,17 @@ def _load_script() -> ModuleType: return module -def test_reusable_scheduler_is_job_level_immutably_pinned_and_least_privileged() -> None: - """Call the pinned scheduler as a job without forwarding reviewer credentials.""" +def test_hourly_audit_does_not_create_a_second_pr_writer() -> None: + """The Orgmetra heartbeat must stay read-only beside the central writer.""" text = WORKFLOW.read_text(encoding="utf-8") - scheduler_block = text.split(" scheduler-sweep:\n", 1)[1].split( - "\n gap-baseline-freshness:", 1 - )[0] - - assert "\n steps:" not in scheduler_block - assert "@main" not in scheduler_block - assert re.search( - rf"^ uses: {re.escape(CENTRAL_SCHEDULER)}@[0-9a-f]{{40}}$", - scheduler_block, - re.MULTILINE, - ) - # The central owner already supports OIDC app-token exchange. The caller - # must not forward repository/org secrets such as independent-review tokens. - assert "secrets: inherit" not in scheduler_block + + assert CENTRAL_SCHEDULER not in text + assert "contents: write" not in text + assert "pull-requests: write" not in text + assert "actions: write" not in text + assert "id-token: write" not in text + assert "secrets: inherit" not in text + assert "permissions:\n contents: read\n pull-requests: read\n issues: read" in text def test_live_queue_counts_request_all_pages(monkeypatch) -> None: From 928948b54b39222be43b99637c2f81892a05b6ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 04:21:11 -0700 Subject: [PATCH 11/20] fix(ops): preserve central scheduler as sole PR writer --- .github/workflows/hourly-pr-gap-loop.yml | 55 ++++++------------------ 1 file changed, 12 insertions(+), 43 deletions(-) diff --git a/.github/workflows/hourly-pr-gap-loop.yml b/.github/workflows/hourly-pr-gap-loop.yml index 71f7f6c0d..31ef62aaa 100644 --- a/.github/workflows/hourly-pr-gap-loop.yml +++ b/.github/workflows/hourly-pr-gap-loop.yml @@ -1,63 +1,32 @@ -name: Hourly PR and Gap Loop +name: Hourly Gap Baseline Freshness Audit -# Durable execution-loop driver for Orgmetra. +# Read-only Orgmetra heartbeat for buyer-facing gap truth. # -# Contract (docs/product-technical-gap-baseline.md "Execution loop"): -# each hour this workflow (1) re-runs the TRUSTED central review/merge -# scheduler so approval-ready heads keep integrating even when no PR event -# fires, and (2) audits docs/product-technical-gap-baseline.md against live -# repository truth so the next development iteration starts from fresh state. -# -# The central scheduler is called only through an immutable reviewed commit. -# Its mutation permissions are scoped to that reusable-workflow job; the local -# freshness audit remains read-only and never self-approves or bypasses rules. -# Repository/organization secrets are deliberately not inherited: the central -# owner already supports OIDC app-token exchange, so independent-review or PAT -# credentials do not cross this caller boundary. +# The central ContextualWisdomLab/.github scheduler already owns review dispatch, +# branch updates and protected PR integration on its established cadence. This +# workflow deliberately does not invoke or duplicate that writer. It only audits +# docs/product-technical-gap-baseline.md against fresh GitHub state so a quiet +# repository cannot leave buyer-facing baseline copy silently stale. on: schedule: - # Hourly, offset from quarter-hour org sweeps to avoid provider contention. - cron: "37 * * * *" workflow_dispatch: -permissions: {} +permissions: + contents: read + pull-requests: read + issues: read concurrency: - group: hourly-pr-gap-loop + group: hourly-gap-baseline-freshness cancel-in-progress: false jobs: - scheduler-sweep: - name: Trusted review and merge sweep - uses: ContextualWisdomLab/.github/.github/workflows/pr-review-merge-scheduler.yml@8fd471a31399a914d9cb22a840f4a4c68e010ea6 - permissions: - actions: write - checks: read - contents: write - id-token: write - pull-requests: write - with: - dry_run: false - max_prs: "100" - trigger_reviews: true - review_dispatch_limit: "2" - branch_update_limit: "2" - enable_auto_merge: true - merge_mode: direct_or_auto - update_branches: true - stale_opencode_minutes: "90" - project_flow: git-flow - base_branch: develop - gap-baseline-freshness: name: Gap baseline freshness audit runs-on: ubuntu-latest timeout-minutes: 10 - permissions: - contents: read - pull-requests: read - issues: read steps: - name: Checkout default branch truth uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 From 4ffdcdbba90dd0b2ab292beb2f4f127cb04359cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 04:21:50 -0700 Subject: [PATCH 12/20] docs(ops): align references with single-writer audit boundary --- docs/doctoring/hourly-pr-gap-loop-references.md | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/doctoring/hourly-pr-gap-loop-references.md b/docs/doctoring/hourly-pr-gap-loop-references.md index 60d2820d4..cc88ca791 100644 --- a/docs/doctoring/hourly-pr-gap-loop-references.md +++ b/docs/doctoring/hourly-pr-gap-loop-references.md @@ -1,18 +1,16 @@ -# Hourly PR/gap-loop primary references +# Hourly gap-baseline audit primary references -Verified against GitHub's current official documentation on **2026-08-25**. These sources define the syntax and trust-boundary assumptions used by the active Orgmetra hourly PR/gap-loop PR; they do not replace the central `.github` repository's published scheduler contract or Orgmetra's effective ruleset. +Verified against GitHub's current official documentation on **2026-08-25**. These sources define the execution and least-privilege assumptions used by the active Orgmetra read-only freshness audit. They do not replace the central `.github` repository's published scheduler contract or Orgmetra's effective ruleset. ## APA 7 references -GitHub. (n.d.). *Reuse workflows*. GitHub Docs. Retrieved August 25, 2026, from https://docs.github.com/en/actions/how-tos/reuse-automations/reuse-workflows +GitHub. (n.d.). *Workflow syntax for GitHub Actions*. GitHub Docs. Retrieved August 25, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax GitHub. (n.d.). *Reusing workflow configurations*. GitHub Docs. Retrieved August 25, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/reusing-workflow-configurations -GitHub. (n.d.). *Workflow syntax for GitHub Actions*. GitHub Docs. Retrieved August 25, 2026, from https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax - ## Decision relevance -- GitHub documents reusable workflows as job-level `jobs..uses` calls, not step-level actions. The supported calling-job keywords include `uses`, `with`, `secrets`, `permissions`, `needs`, `if`, `strategy`, and `concurrency`. -- GitHub documents a full commit SHA as the safest reusable-workflow reference for stability and security. Orgmetra therefore pins the read-only central owner contract to one freshly verified commit instead of following mutable `@main` at execution time. -- GitHub documents `secrets: inherit` as forwarding all secrets available to the caller into the directly called workflow. Because the central scheduler already supports OIDC app-token exchange, the Orgmetra caller deliberately does **not** inherit repository/organization secrets; this keeps independent-review credentials outside the caller boundary. -- The central scheduler remains the owner of review dispatch and protected mutations. Orgmetra only supplies bounded inputs and job permissions required by that published reusable-workflow contract. +- GitHub permits workflow- and job-level `permissions` to constrain `GITHUB_TOKEN`. The Orgmetra heartbeat needs only repository contents, pull-request metadata and issue metadata reads, so its token remains read-only. +- GitHub defines reusable workflow calls as writer-capable job boundaries when the caller grants write permissions. Orgmetra deliberately does **not** call the central review/merge scheduler from this hourly workflow because the central `.github` automation already owns that mutation lane on its established cadence. +- The scheduled audit runs from Orgmetra's default branch after integration and only reports whether the buyer-facing gap baseline is current. It does not dispatch reviews, update branches, approve, enable auto-merge, merge, forward secrets, or obtain an OIDC mutation token. +- The central scheduler remains the single writer for review dispatch and protected PR integration. This active PR only adds a read-only truth-audit surface. From 4bd72889c50205590a67c5e4bc626819e588a50d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 06:12:00 -0700 Subject: [PATCH 13/20] test(ops): fail first on gap freshness time and fetch semantics --- tests/test_hourly_pr_gap_loop.py | 74 ++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/tests/test_hourly_pr_gap_loop.py b/tests/test_hourly_pr_gap_loop.py index 047b80499..fd131645d 100644 --- a/tests/test_hourly_pr_gap_loop.py +++ b/tests/test_hourly_pr_gap_loop.py @@ -99,3 +99,77 @@ def test_missing_baseline_is_nonfatal_until_dependency_integrates( assert module.main() == 0 assert "baseline not present" in capsys.readouterr().out.lower() + + +def test_same_inventory_day_integration_is_not_newer( + monkeypatch, tmp_path, capsys +) -> None: + """A same-Korea-calendar-day commit must not make a date-only snapshot stale.""" + module = _load_script() + baseline = tmp_path / "product-technical-gap-baseline.md" + baseline.write_text("Inventory date: 2026-08-25 (Asia/Seoul).\n", encoding="utf-8") + monkeypatch.setattr( + module, + "_live_state", + lambda: { + "open_pull_requests": 4, + "open_issues": 1, + "newest_develop_commit_date": "2026-08-25T12:00:00Z", + }, + ) + monkeypatch.setattr( + module.sys, + "argv", + ["gap-baseline-freshness", "--baseline", str(baseline)], + ) + + assert module.main() == 0 + assert "result: current" in capsys.readouterr().out.lower() + + +def test_next_korea_calendar_day_integration_requires_refresh( + monkeypatch, tmp_path, capsys +) -> None: + """UTC timestamps crossing midnight in Korea must stale the prior local date.""" + module = _load_script() + baseline = tmp_path / "product-technical-gap-baseline.md" + baseline.write_text("Inventory date: 2026-08-25 (Asia/Seoul).\n", encoding="utf-8") + monkeypatch.setattr( + module, + "_live_state", + lambda: { + "open_pull_requests": 4, + "open_issues": 1, + "newest_develop_commit_date": "2026-08-25T16:00:00Z", + }, + ) + monkeypatch.setattr( + module.sys, + "argv", + ["gap-baseline-freshness", "--baseline", str(baseline)], + ) + + assert module.main() == 0 + assert "refresh candidate" in capsys.readouterr().out.lower() + + +def test_live_state_failure_is_nonpassing(monkeypatch, tmp_path, capsys) -> None: + """An unavailable live control plane must fail closed instead of silently passing.""" + module = _load_script() + baseline = tmp_path / "product-technical-gap-baseline.md" + baseline.write_text("Inventory date: 2026-08-25 (Asia/Seoul).\n", encoding="utf-8") + + def fail_live_state() -> dict[str, object]: + raise RuntimeError("GitHub API unavailable") + + monkeypatch.setattr(module, "_live_state", fail_live_state) + monkeypatch.setattr( + module.sys, + "argv", + ["gap-baseline-freshness", "--baseline", str(baseline)], + ) + + assert module.main() == 2 + output = capsys.readouterr().out.lower() + assert "fail live-state fetch" in output + assert "github api unavailable" in output From 77e726521fd879e18f169b3d44bf0d2ab1c378c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 06:12:52 -0700 Subject: [PATCH 14/20] fix(ops): make gap freshness calendar-aware and fail closed --- scripts/ops/gap_baseline_freshness.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/scripts/ops/gap_baseline_freshness.py b/scripts/ops/gap_baseline_freshness.py index b64a69f30..0935c03ed 100644 --- a/scripts/ops/gap_baseline_freshness.py +++ b/scripts/ops/gap_baseline_freshness.py @@ -9,7 +9,7 @@ Exit codes: 0 audit completed, or the baseline has not integrated yet; findings are reported - 2 contract violation (an existing baseline is unreadable/invalid) + 2 contract violation or live-state evidence could not be established The output is plain Markdown so callers can append it directly to ``$GITHUB_STEP_SUMMARY``. @@ -25,11 +25,13 @@ import sys from datetime import datetime, timezone from pathlib import Path +from zoneinfo import ZoneInfo INVENTORY_DATE_PATTERN = re.compile( r"^Inventory date:\s*(\d{4}-\d{2}-\d{2})", re.MULTILINE ) REPOSITORY_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") +BASELINE_TIMEZONE = ZoneInfo("Asia/Seoul") def _run_gh_json( @@ -129,26 +131,24 @@ def main() -> int: print("gap-baseline freshness: FAIL missing 'Inventory date:' header") return 2 - inventory_date = datetime.strptime(match.group(1), "%Y-%m-%d").replace( - tzinfo=timezone.utc - ) - now = datetime.now(tz=timezone.utc) - age_days = (now - inventory_date).total_seconds() / 86400 + inventory_date = datetime.strptime(match.group(1), "%Y-%m-%d").date() + now_date = datetime.now(tz=BASELINE_TIMEZONE).date() + age_days = (now_date - inventory_date).days lines = [ "## Gap baseline freshness", "", f"- Baseline file: `{baseline_path.as_posix()}`", - f"- Inventory date: {inventory_date.date().isoformat()} " - f"(age {age_days:.1f} days)", + f"- Inventory date: {inventory_date.isoformat()} " + f"(age {float(age_days):.1f} days)", ] try: state = _live_state() except (RuntimeError, ValueError, KeyError) as error: - lines.append(f"- Live-state fetch failed (transient?): {error}") + lines.append(f"- FAIL live-state fetch: {error}") print("\n".join(lines)) - return 0 + return 2 lines.extend( [ @@ -165,7 +165,9 @@ def main() -> int: integration_time = datetime.fromisoformat( newest_integration.replace("Z", "+00:00") ) - integrations_after_snapshot = integration_time > inventory_date + integrations_after_snapshot = ( + integration_time.astimezone(BASELINE_TIMEZONE).date() > inventory_date + ) if integrations_after_snapshot: lines.append( From 6f9d540d8f6751a17a2c7489f14b888a71d4be02 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 06:13:38 -0700 Subject: [PATCH 15/20] docs(ops): document fail-closed freshness semantics --- docs/doctoring/hourly-pr-gap-loop-references.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/doctoring/hourly-pr-gap-loop-references.md b/docs/doctoring/hourly-pr-gap-loop-references.md index cc88ca791..4560f08a5 100644 --- a/docs/doctoring/hourly-pr-gap-loop-references.md +++ b/docs/doctoring/hourly-pr-gap-loop-references.md @@ -13,4 +13,6 @@ GitHub. (n.d.). *Reusing workflow configurations*. GitHub Docs. Retrieved August - GitHub permits workflow- and job-level `permissions` to constrain `GITHUB_TOKEN`. The Orgmetra heartbeat needs only repository contents, pull-request metadata and issue metadata reads, so its token remains read-only. - GitHub defines reusable workflow calls as writer-capable job boundaries when the caller grants write permissions. Orgmetra deliberately does **not** call the central review/merge scheduler from this hourly workflow because the central `.github` automation already owns that mutation lane on its established cadence. - The scheduled audit runs from Orgmetra's default branch after integration and only reports whether the buyer-facing gap baseline is current. It does not dispatch reviews, update branches, approve, enable auto-merge, merge, forward secrets, or obtain an OIDC mutation token. +- The baseline's `Inventory date` is explicitly an **Asia/Seoul calendar date**, not a midnight-UTC timestamp. A `develop` commit later on the same Korea calendar day therefore does not by itself make a date-only snapshot stale; a commit on a later Korea calendar day does. +- Live GitHub state is authoritative evidence for this audit. If the PR/issue/commit reads cannot be established or their payload shape is invalid, the audit fails closed with a non-zero result rather than reporting a successful/current audit from missing evidence. - The central scheduler remains the single writer for review dispatch and protected PR integration. This active PR only adds a read-only truth-audit surface. From 3c9a31f416cbf760741d957f9146deea06b8c643 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 06:15:51 -0700 Subject: [PATCH 16/20] chore(ops): remove stale timezone import --- scripts/ops/gap_baseline_freshness.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/ops/gap_baseline_freshness.py b/scripts/ops/gap_baseline_freshness.py index 0935c03ed..11de44000 100644 --- a/scripts/ops/gap_baseline_freshness.py +++ b/scripts/ops/gap_baseline_freshness.py @@ -23,7 +23,7 @@ import re import subprocess import sys -from datetime import datetime, timezone +from datetime import datetime from pathlib import Path from zoneinfo import ZoneInfo From b59820f81e34e196c418ee1876e571e60a0dd166 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 06:25:08 -0700 Subject: [PATCH 17/20] test(ops): reject future-dated gap inventory before live reads --- tests/test_hourly_pr_gap_loop.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_hourly_pr_gap_loop.py b/tests/test_hourly_pr_gap_loop.py index fd131645d..eaf910557 100644 --- a/tests/test_hourly_pr_gap_loop.py +++ b/tests/test_hourly_pr_gap_loop.py @@ -101,6 +101,36 @@ def test_missing_baseline_is_nonfatal_until_dependency_integrates( assert "baseline not present" in capsys.readouterr().out.lower() +def test_future_inventory_date_is_nonpassing_without_live_read( + monkeypatch, tmp_path, capsys +) -> None: + """A future-dated buyer snapshot cannot be accepted as current repository truth.""" + module = _load_script() + baseline = tmp_path / "product-technical-gap-baseline.md" + baseline.write_text("Inventory date: 2999-12-31 (Asia/Seoul).\n", encoding="utf-8") + live_calls = 0 + + def live_state() -> dict[str, object]: + nonlocal live_calls + live_calls += 1 + return { + "open_pull_requests": 4, + "open_issues": 1, + "newest_develop_commit_date": "2026-08-25T12:00:00Z", + } + + monkeypatch.setattr(module, "_live_state", live_state) + monkeypatch.setattr( + module.sys, + "argv", + ["gap-baseline-freshness", "--baseline", str(baseline)], + ) + + assert module.main() == 2 + assert live_calls == 0 + assert "future inventory date" in capsys.readouterr().out.lower() + + def test_same_inventory_day_integration_is_not_newer( monkeypatch, tmp_path, capsys ) -> None: From 72819cb9cf8ce2e231768f88154eac31da0b7c93 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 06:25:40 -0700 Subject: [PATCH 18/20] fix(ops): fail closed on future-dated gap inventory --- scripts/ops/gap_baseline_freshness.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/ops/gap_baseline_freshness.py b/scripts/ops/gap_baseline_freshness.py index 11de44000..2fa133e2c 100644 --- a/scripts/ops/gap_baseline_freshness.py +++ b/scripts/ops/gap_baseline_freshness.py @@ -133,6 +133,13 @@ def main() -> int: inventory_date = datetime.strptime(match.group(1), "%Y-%m-%d").date() now_date = datetime.now(tz=BASELINE_TIMEZONE).date() + if inventory_date > now_date: + print( + "gap-baseline freshness: FAIL future inventory date " + f"{inventory_date.isoformat()} exceeds current Asia/Seoul date " + f"{now_date.isoformat()}" + ) + return 2 age_days = (now_date - inventory_date).days lines = [ From ece19f3e36939457b2901dd3a55b7be7c6cc52be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 06:26:02 -0700 Subject: [PATCH 19/20] docs(ops): record future-inventory fail-closed contract --- docs/doctoring/hourly-pr-gap-loop-references.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/doctoring/hourly-pr-gap-loop-references.md b/docs/doctoring/hourly-pr-gap-loop-references.md index 4560f08a5..d8028a91c 100644 --- a/docs/doctoring/hourly-pr-gap-loop-references.md +++ b/docs/doctoring/hourly-pr-gap-loop-references.md @@ -14,5 +14,6 @@ GitHub. (n.d.). *Reusing workflow configurations*. GitHub Docs. Retrieved August - GitHub defines reusable workflow calls as writer-capable job boundaries when the caller grants write permissions. Orgmetra deliberately does **not** call the central review/merge scheduler from this hourly workflow because the central `.github` automation already owns that mutation lane on its established cadence. - The scheduled audit runs from Orgmetra's default branch after integration and only reports whether the buyer-facing gap baseline is current. It does not dispatch reviews, update branches, approve, enable auto-merge, merge, forward secrets, or obtain an OIDC mutation token. - The baseline's `Inventory date` is explicitly an **Asia/Seoul calendar date**, not a midnight-UTC timestamp. A `develop` commit later on the same Korea calendar day therefore does not by itself make a date-only snapshot stale; a commit on a later Korea calendar day does. +- A baseline inventory date later than the current Asia/Seoul calendar date is internally impossible evidence and fails closed before any live GitHub read. A future-dated snapshot must never be reported as current merely because no `develop` commit is later than that future date. - Live GitHub state is authoritative evidence for this audit. If the PR/issue/commit reads cannot be established or their payload shape is invalid, the audit fails closed with a non-zero result rather than reporting a successful/current audit from missing evidence. - The central scheduler remains the single writer for review dispatch and protected PR integration. This active PR only adds a read-only truth-audit surface. From de2b0aa5e6cd226815b25f7e83a8130c6f124a6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 06:43:14 +0900 Subject: [PATCH 20/20] fix(ops): fail closed on stale gap queue evidence --- .github/workflows/hourly-pr-gap-loop.yml | 1 + .../hourly-pr-gap-loop-references.md | 1 + scripts/ops/gap_baseline_freshness.py | 71 ++++++++++++++-- tests/test_hourly_pr_gap_loop.py | 83 ++++++++++++++++++- 4 files changed, 146 insertions(+), 10 deletions(-) diff --git a/.github/workflows/hourly-pr-gap-loop.yml b/.github/workflows/hourly-pr-gap-loop.yml index 31ef62aaa..500cc7b58 100644 --- a/.github/workflows/hourly-pr-gap-loop.yml +++ b/.github/workflows/hourly-pr-gap-loop.yml @@ -31,6 +31,7 @@ jobs: - name: Checkout default branch truth uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: + ref: ${{ github.event.repository.default_branch }} persist-credentials: false - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 diff --git a/docs/doctoring/hourly-pr-gap-loop-references.md b/docs/doctoring/hourly-pr-gap-loop-references.md index d8028a91c..8bd9f3176 100644 --- a/docs/doctoring/hourly-pr-gap-loop-references.md +++ b/docs/doctoring/hourly-pr-gap-loop-references.md @@ -14,6 +14,7 @@ GitHub. (n.d.). *Reusing workflow configurations*. GitHub Docs. Retrieved August - GitHub defines reusable workflow calls as writer-capable job boundaries when the caller grants write permissions. Orgmetra deliberately does **not** call the central review/merge scheduler from this hourly workflow because the central `.github` automation already owns that mutation lane on its established cadence. - The scheduled audit runs from Orgmetra's default branch after integration and only reports whether the buyer-facing gap baseline is current. It does not dispatch reviews, update branches, approve, enable auto-merge, merge, forward secrets, or obtain an OIDC mutation token. - The baseline's `Inventory date` is explicitly an **Asia/Seoul calendar date**, not a midnight-UTC timestamp. A `develop` commit later on the same Korea calendar day therefore does not by itself make a date-only snapshot stale; a commit on a later Korea calendar day does. +- The audit compares the recorded open pull-request and non-PR issue counts with the complete live queues. A queue change is reported as a refresh candidate even when `develop` has not advanced, because active-PR and issue truth can change without a protected-branch commit. - A baseline inventory date later than the current Asia/Seoul calendar date is internally impossible evidence and fails closed before any live GitHub read. A future-dated snapshot must never be reported as current merely because no `develop` commit is later than that future date. - Live GitHub state is authoritative evidence for this audit. If the PR/issue/commit reads cannot be established or their payload shape is invalid, the audit fails closed with a non-zero result rather than reporting a successful/current audit from missing evidence. - The central scheduler remains the single writer for review dispatch and protected PR integration. This active PR only adds a read-only truth-audit surface. diff --git a/scripts/ops/gap_baseline_freshness.py b/scripts/ops/gap_baseline_freshness.py index 2fa133e2c..49a002296 100644 --- a/scripts/ops/gap_baseline_freshness.py +++ b/scripts/ops/gap_baseline_freshness.py @@ -30,8 +30,27 @@ INVENTORY_DATE_PATTERN = re.compile( r"^Inventory date:\s*(\d{4}-\d{2}-\d{2})", re.MULTILINE ) +SNAPSHOT_QUEUE_PATTERN = re.compile( + r"^At this snapshot,\s+(?P\d+)\s+pull requests\s+" + r"and\s+(?P\d+|zero|one|two|three|four|five|six|seven|" + r"eight|nine|ten)\s+non-PR issues?(?:\s+\([^)]*\))?\s+are open\b", + re.MULTILINE, +) REPOSITORY_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") BASELINE_TIMEZONE = ZoneInfo("Asia/Seoul") +COUNT_WORDS = { + "zero": 0, + "one": 1, + "two": 2, + "three": 3, + "four": 4, + "five": 5, + "six": 6, + "seven": 7, + "eight": 8, + "nine": 9, + "ten": 10, +} def _run_gh_json( @@ -74,6 +93,18 @@ def _live_repository() -> str: return repository +def _snapshot_queue_counts(baseline_text: str) -> tuple[int, int]: + """Read the recorded open PR and non-PR issue counts from the baseline.""" + match = SNAPSHOT_QUEUE_PATTERN.search(baseline_text) + if match is None: + raise RuntimeError("missing snapshot open queue counts") + issue_count = match.group("open_issues") + parsed_issue_count = ( + int(issue_count) if issue_count.isdigit() else COUNT_WORDS[issue_count] + ) + return int(match.group("open_pull_requests")), parsed_issue_count + + def _live_state() -> dict[str, object]: """Fetch complete live open PR/issue queues and newest develop integration.""" repository = _live_repository() @@ -90,6 +121,8 @@ def _live_state() -> dict[str, object]: commits = _run_gh_json( ["api", f"repos/{repository}/commits?sha=develop&per_page=1"] ) + if not commits: + raise RuntimeError("develop commit payload is empty") newest_commit_date = None if commits: commit = commits[0].get("commit") @@ -141,6 +174,13 @@ def main() -> int: ) return 2 age_days = (now_date - inventory_date).days + try: + snapshot_open_pull_requests, snapshot_open_issues = _snapshot_queue_counts( + baseline_text + ) + except (RuntimeError, ValueError) as error: + print(f"gap-baseline freshness: FAIL malformed baseline: {error}") + return 2 lines = [ "## Gap baseline freshness", @@ -148,6 +188,8 @@ def main() -> int: f"- Baseline file: `{baseline_path.as_posix()}`", f"- Inventory date: {inventory_date.isoformat()} " f"(age {float(age_days):.1f} days)", + f"- Snapshot open pull requests: {snapshot_open_pull_requests}", + f"- Snapshot open issues: {snapshot_open_issues}", ] try: @@ -168,20 +210,37 @@ def main() -> int: newest_integration = state.get("newest_develop_commit_date") integrations_after_snapshot = False - if isinstance(newest_integration, str): + try: + if not isinstance(newest_integration, str): + raise ValueError("newest develop integration timestamp is missing") integration_time = datetime.fromisoformat( newest_integration.replace("Z", "+00:00") ) integrations_after_snapshot = ( integration_time.astimezone(BASELINE_TIMEZONE).date() > inventory_date ) + except (TypeError, ValueError) as error: + lines.append(f"- FAIL live-state fetch: invalid develop timestamp: {error}") + print("\n".join(lines)) + return 2 + + queue_changed = ( + state["open_pull_requests"] != snapshot_open_pull_requests + or state["open_issues"] != snapshot_open_issues + ) - if integrations_after_snapshot: + if integrations_after_snapshot or queue_changed: + reasons = [] + if integrations_after_snapshot: + reasons.append("develop integrated commits after the recorded inventory date") + if queue_changed: + reasons.append("the live open PR/issue queue differs from the snapshot") lines.append( - "- Result: **refresh candidate** — `develop` integrated commits after " - "the recorded inventory date. Per the execution loop, refresh the " - "baseline only when buyer/product-visible truth changed; otherwise " - "record this audit as observed." + "- Result: **refresh candidate** — " + + " and ".join(reasons) + + ". Per the execution loop, refresh the baseline only when " + "buyer/product-visible truth changed; otherwise record this audit " + "as observed." ) else: lines.append( diff --git a/tests/test_hourly_pr_gap_loop.py b/tests/test_hourly_pr_gap_loop.py index eaf910557..c57c97b0f 100644 --- a/tests/test_hourly_pr_gap_loop.py +++ b/tests/test_hourly_pr_gap_loop.py @@ -11,6 +11,10 @@ WORKFLOW = ROOT / ".github" / "workflows" / "hourly-pr-gap-loop.yml" SCRIPT = ROOT / "scripts" / "ops" / "gap_baseline_freshness.py" CENTRAL_SCHEDULER = "pr-review-merge-scheduler.yml" +BASELINE = ( + "Inventory date: 2026-08-25 (Asia/Seoul).\n" + "At this snapshot, 4 pull requests and one non-PR issue are open.\n" +) def _load_script() -> ModuleType: @@ -32,6 +36,7 @@ def test_hourly_audit_does_not_create_a_second_pr_writer() -> None: assert "id-token: write" not in text assert "secrets: inherit" not in text assert "permissions:\n contents: read\n pull-requests: read\n issues: read" in text + assert "ref: ${{ github.event.repository.default_branch }}" in text def test_live_queue_counts_request_all_pages(monkeypatch) -> None: @@ -107,7 +112,9 @@ def test_future_inventory_date_is_nonpassing_without_live_read( """A future-dated buyer snapshot cannot be accepted as current repository truth.""" module = _load_script() baseline = tmp_path / "product-technical-gap-baseline.md" - baseline.write_text("Inventory date: 2999-12-31 (Asia/Seoul).\n", encoding="utf-8") + baseline.write_text( + BASELINE.replace("2026-08-25", "2999-12-31"), encoding="utf-8" + ) live_calls = 0 def live_state() -> dict[str, object]: @@ -137,7 +144,7 @@ def test_same_inventory_day_integration_is_not_newer( """A same-Korea-calendar-day commit must not make a date-only snapshot stale.""" module = _load_script() baseline = tmp_path / "product-technical-gap-baseline.md" - baseline.write_text("Inventory date: 2026-08-25 (Asia/Seoul).\n", encoding="utf-8") + baseline.write_text(BASELINE, encoding="utf-8") monkeypatch.setattr( module, "_live_state", @@ -163,7 +170,7 @@ def test_next_korea_calendar_day_integration_requires_refresh( """UTC timestamps crossing midnight in Korea must stale the prior local date.""" module = _load_script() baseline = tmp_path / "product-technical-gap-baseline.md" - baseline.write_text("Inventory date: 2026-08-25 (Asia/Seoul).\n", encoding="utf-8") + baseline.write_text(BASELINE, encoding="utf-8") monkeypatch.setattr( module, "_live_state", @@ -187,7 +194,7 @@ def test_live_state_failure_is_nonpassing(monkeypatch, tmp_path, capsys) -> None """An unavailable live control plane must fail closed instead of silently passing.""" module = _load_script() baseline = tmp_path / "product-technical-gap-baseline.md" - baseline.write_text("Inventory date: 2026-08-25 (Asia/Seoul).\n", encoding="utf-8") + baseline.write_text(BASELINE, encoding="utf-8") def fail_live_state() -> dict[str, object]: raise RuntimeError("GitHub API unavailable") @@ -203,3 +210,71 @@ def fail_live_state() -> dict[str, object]: output = capsys.readouterr().out.lower() assert "fail live-state fetch" in output assert "github api unavailable" in output + + +def test_queue_change_is_reported_as_refresh_candidate( + monkeypatch, tmp_path, capsys +) -> None: + """A changed live queue makes the point-in-time baseline a refresh candidate.""" + module = _load_script() + baseline = tmp_path / "product-technical-gap-baseline.md" + baseline.write_text(BASELINE, encoding="utf-8") + monkeypatch.setattr( + module, + "_live_state", + lambda: { + "open_pull_requests": 5, + "open_issues": 1, + "newest_develop_commit_date": "2026-08-25T12:00:00Z", + }, + ) + monkeypatch.setattr( + module.sys, + "argv", + ["gap-baseline-freshness", "--baseline", str(baseline)], + ) + + assert module.main() == 0 + assert "refresh candidate" in capsys.readouterr().out.lower() + + +def test_empty_develop_commit_payload_fails_closed(monkeypatch) -> None: + """An empty develop response cannot establish a current integration point.""" + module = _load_script() + + class Completed: + returncode = 0 + stderr = "" + stdout = "[]" + + monkeypatch.setattr(module.subprocess, "run", lambda *_args, **_kwargs: Completed()) + try: + module._live_state() + except RuntimeError as error: + assert "empty" in str(error) + else: + raise AssertionError("empty develop payload was accepted") + + +def test_invalid_develop_timestamp_fails_closed(monkeypatch, tmp_path, capsys) -> None: + """Malformed integration timestamps cannot be reported as a current audit.""" + module = _load_script() + baseline = tmp_path / "product-technical-gap-baseline.md" + baseline.write_text(BASELINE, encoding="utf-8") + monkeypatch.setattr( + module, + "_live_state", + lambda: { + "open_pull_requests": 4, + "open_issues": 1, + "newest_develop_commit_date": "not-a-timestamp", + }, + ) + monkeypatch.setattr( + module.sys, + "argv", + ["gap-baseline-freshness", "--baseline", str(baseline)], + ) + + assert module.main() == 2 + assert "invalid develop timestamp" in capsys.readouterr().out.lower()