Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 137 additions & 54 deletions .github/workflows/opencode-review-dispatch.yml

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Semantic Versioning where the repository publishes a release.

### Changed

- Split central OpenCode publication into distinct surfaces: the formal pull-request review is a source-backed walkthrough of the actual diff, and the issue comment is gate/status only (head SHA, run id, coverage/check results). Coverage-evidence failure no longer replaces the review or cites `.github/workflows/opencode-review.yml:1` on a product repository that did not change that file. After a fallback COMMENT review, restore the status comment to `COVERAGE_BLOCKED` so a model-unavailable coverage miss cannot look like a finished comment-only review, and list extracted Rust API names without inventing a class relationship.
- Require the hourly repair worker to establish an exact-head root cause, enumerate the smallest remediation candidates, and prove writer authority, sealed-path scope, credentials, dependency order, verifiability, and causal effect before editing; infeasible or external blockers leave the tree unchanged while the broader loop continues with another eligible PR or buyer-visible product gap.
- Run the bounded Clearfolio PR review-feedback repair caller at minute 23 of every hour while keeping the shared scheduler free of product-specific timers and repository names for modular reuse by naruon, contextual-orchestrator, Inkspan, and other CWL services.
- Run the bounded DiskSage repair heartbeat at minute 37 of every hour, dispatch no more than one exact-head repair, and wait two hours before redispatching an unchanged head so legitimate OpenCode or NVIDIA NIM latency does not create duplicate writers.
Expand All @@ -26,6 +27,8 @@ Semantic Versioning where the repository publishes a release.

### Fixed

- Stopped the OriginWeave-style coverage sandbox false blocker: the isolated image now installs the repository rustup channel plus `llvm-tools-preview`, prefetches `Cargo.lock` crates, and runs `cargo llvm-cov --offline --locked` instead of Debian rustc 1.85 without llvm-tools (ContextualWisdomLab/OriginWeave#47).
- Taught the OpenCode mermaid helper `crates/`, `src/`, and Rust/Python/TypeScript layouts so a three-file crate change is labeled as that crate, not `Changed file (3 files)`.
- Materialized base Python locks only when every package line is an exact SHA-256 pin or a bounded relative `-r`/`--requirement` include. A lone `--require-hashes` directive, a dotted include such as `./lock.txt`, or `-r other-hashes.txt` no longer enters the trusted build context.
- Refused a conflict-scope repository root whose immediate parent is a symbolic link, so a swapped parent cannot redirect the canonical worktree after the last-component check (CWE-367).
- Bounded the Strix quality self-test's deterministic timeout fixtures to 3-second process and 5-second fake-sleep budgets so exact-head policy evidence completes inside the existing job limit without changing production Strix scanner timeouts, providers, credentials, or review semantics.
Expand Down Expand Up @@ -55,6 +58,7 @@ Semantic Versioning where the repository publishes a release.

### Documentation

- Added an APA 7 doctoring record for the OriginWeave #47 OpenCode publication failure: coverage-as-review, duplicate formal-review/issue-comment bodies, generic mermaid inventory, and the rustc 1.85 / missing llvm-tools sandbox RCA.
- Added an APA 7 doctoring record for conflict-control evidence isolation, including the Strix-reported trust-boundary failure, test-first remediation, canonical-path rule, operator contract, rollback, MITRE CWE-22, and current GitHub Actions secure-use guidance.
- Added operator and APA 7 doctoring records for the hourly cadence, immutable source identity, NVIDIA NIM provider and secret boundary, high-reasoning Mistral Small 4 writer, model-process credential isolation, modular MSA ownership, product-specific caller activation, verification contract, and rollback.
- Added DiskSage operational documentation for the hourly RCA loop, bounded retry cadence, permission model, standalone and MSA reuse, verification, rollback, and APA 7 references.
Expand Down
9 changes: 9 additions & 0 deletions ci-review-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ green checks are not runtime-tool receipts. When no receipt exists, describe
only the source trace or explicit execution limitation; fabricating browser or
DevTools evidence invalidates the entire control block.

The formal review must name the actual changed files and what they do,
include file/line findings on the current-head diff or an explicit APPROVE
with a real walkthrough, and draw a useful sequence/class/state diagram of
the changed API rather than a generic `Changed file (N files)` inventory.
Coverage is a gate, not the review: cite coverage evidence in the status
surface and never replace the product-file walkthrough with a coverage
blocker. Never cite `.github/workflows/opencode-review.yml:1` unless that
file is in the current-head diff.

Review the diff first, then inspect surrounding code only when needed to
understand impact. Evaluate correctness, API compatibility, security/privacy,
data integrity, concurrency, error handling, observability, performance,
Expand Down
113 changes: 113 additions & 0 deletions docs/doctoring/opencode-review-surfaces-originweave-47.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# OpenCode review surfaces and OriginWeave coverage sandbox

검토 기준일: **2026-08-16**

## Incident

ContextualWisdomLab/OriginWeave#47, head
`79cf275686e2376a51783a2d03128eca21e7c0e5`, workflow run `31951179896`,
published the same body as both the formal pull-request review and the issue
comment: a generic overview plus one HIGH finding on
`.github/workflows/opencode-review.yml:1` saying coverage-evidence failed. The
pull request actually changed
`crates/originweave-destination/src/lib.rs`, `resolution.rs`, and
`tests/resolution_freshness.rs` (FreshResolutionSnapshot / DNS-rebinding
TOCTOU). The mermaid inventory said `Changed file (3 files)` because unknown
paths, including `crates/`, were bucketed as "Changed file". Repository CI on
that head passed. The central isolated coverage job failed and replaced the
entire review.

## Root cause

When `needs.coverage-evidence.result != success`, the publisher synthesized
`REQUEST_CHANGES`, posted it with `gh pr review` and again as an issue comment,
and exited before the model pool could review the diff. The coverage sandbox
false blocker was Debian rustc 1.85 plus cargo-llvm-cov 0.8.7 without
`llvm-tools-preview`, in a `--network=none` image, against a workspace that
declares `rust-version = "1.97"` and `edition = "2024"`. The job log was
`failed to find llvm-tools-preview`. The default 100% line threshold was not
the false blocker: OriginWeave also requires 100% in repository CI.

## Decision

Coverage remains a fail-closed gate. It is no longer the review.

1. The formal pull-request review is a source-backed walkthrough of the
current-head product diff, including a fallback review that names the
changed crate files when the model pool did not emit a control block.
2. The issue comment is gate/status only: head SHA, run id, coverage/check
results, and the hidden approve-gate control block. It must not repeat
`## Pull request overview` or `## Findings`.
3. A coverage miss, skip, or unsupported-tooling result blocks approval and
fails the required review job after the diff review is published. It must
not cite `.github/workflows/opencode-review.yml:1` unless that file is in
the pull-request diff.
4. The trusted coverage image materializes bounded `Cargo.toml` /
`Cargo.lock` / `rust-toolchain.toml` / workspace member manifests, installs
the declared rustup channel with `llvm-tools-preview` when it is newer than
Debian rustc 1.85, prefetches the lockfile, and runs
`cargo llvm-cov --offline --locked`. A real coverage miss still fails.

Read-only review-agent permissions, NVIDIA NIM-first routing
(`NVIDIA_NIM_API_KEY` bound into `NVIDIA_API_KEY`), OpenCode CLI 1.17.13, and
the existing review-bot identity are unchanged. `COPILOT_GITHUB_TOKEN` is not
introduced.

## Verification contract

Regression tests prove that:

1. the formal review body is not equal to the status comment;
2. a coverage-gate failure still produces a review that names the changed
crate files;
3. no finding is anchored to `opencode-review.yml:1` unless that file is in
the diff;
4. mermaid labels a `crates/...` change as a Rust crate surface, not
`Changed file (3 files)`;
5. the publisher function
`request_changes_for_coverage_evidence_failure` updates the status comment
and does not call `create_pull_review`;
6. the model pool still runs when coverage-evidence failed (`!= cancelled`);
7. `publish_fallback_diff_review` restores `COVERAGE_BLOCKED` after the
COMMENT review so `create_pull_review` cannot leave `Gate result: COMMENT`
on a coverage miss;
8. the class diagram lists public items and does not invent
`FirstType --> SecondType`; and
9. bounded Rust toolchain materialization copies manifests only, selects
rustup 1.97 for OriginWeave-style workspaces, and rejects parent-directory
members and symlinks.

## Limitations

A later rustup or llvm-tools catalog change can still fail the image build.
That failure remains a coverage-gate failure, not a synthesized product-file
finding. The sandbox does not weaken a genuine below-threshold coverage miss.

Publishing the fallback review uses `create_pull_review COMMENT`, which also
rewrites the issue comment to `Gate result: COMMENT`. The publisher must then
restore `COVERAGE_BLOCKED` on that status surface so a model-unavailable
coverage miss does not look like a completed comment-only review. The fallback
class diagram lists extracted public items and does not invent a relationship
between the first two names.

## References

GitHub, Inc. (n.d.). *REST API endpoints for pull request reviews*. GitHub
Docs. Retrieved August 16, 2026, from
https://docs.github.com/en/rest/pulls/reviews

International Organization for Standardization. (2023). *Systems and software
engineering — Systems and software Quality Requirements and Evaluation
(SQuaRE) — Product quality model* (ISO/IEC 25010:2023).
https://www.iso.org/standard/78176.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

Rust Project Developers. (n.d.). *The rustup book*. Retrieved August 16, 2026,
from https://rust-lang.github.io/rustup/

Taiki Endo. (2026). *cargo-llvm-cov* (Version 0.8.7) [Computer software].
https://github.com/taiki-e/cargo-llvm-cov
235 changes: 235 additions & 0 deletions scripts/ci/materialize_base_rust_toolchain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
#!/usr/bin/env python3
"""Copy bounded Rust workspace inputs into the trusted coverage image context.

The isolated coverage sandbox is networkless and previously used Debian rustc
1.85 without ``llvm-tools-preview``. OriginWeave-style workspaces declare
``rust-version = "1.97"`` and ``edition = "2024"``, so the image must install
the repository toolchain plus llvm-tools and prefetch ``Cargo.lock`` crates
before the sandbox starts.
"""

from __future__ import annotations

import argparse
import json
import re
import shutil
import subprocess
import sys
from pathlib import Path, PurePosixPath
from typing import Any

try:
import tomllib
except ModuleNotFoundError: # pragma: no cover - exercised by Python 3.10 CI.
import tomli as tomllib

DEBIAN_RUSTC = (1, 85, 0)
CHANNEL_RE = re.compile(r"^[A-Za-z0-9._+-]+$")
VERSION_RE = re.compile(r"^(\d+)\.(\d+)(?:\.(\d+))?$")
RUST_INPUT_NAMES = ("rust-toolchain.toml", "rust-toolchain", "Cargo.toml", "Cargo.lock")


def _git(repo_root: Path, *args: str) -> bytes:
"""Run one read-only git command in the materialized merge tree."""
completed = subprocess.run(
["git", "-C", str(repo_root), *args],
check=False,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if completed.returncode != 0:
stderr = completed.stderr.decode("utf-8", errors="replace").strip()
raise RuntimeError(f"git {args[0]} failed: {stderr}")
return completed.stdout


def parse_rust_version(value: str) -> tuple[int, int, int] | None:
"""Parse a rust-version or toolchain channel into a comparable triple."""
match = VERSION_RE.fullmatch(value.strip())
if match is None:
return None
return (int(match.group(1)), int(match.group(2)), int(match.group(3) or 0))


def _nested(document: dict[str, Any], path: str) -> Any:
"""Return a dotted TOML value, or None when any segment is absent."""
value: Any = document
for segment in path.split("."):
if not isinstance(value, dict) or segment not in value:
return None
value = value[segment]
return value


def read_toml(path: Path) -> dict[str, Any]:
"""Load one TOML document as a mapping."""
document = tomllib.loads(path.read_text(encoding="utf-8"))
if not isinstance(document, dict):
raise ValueError(f"{path} must contain a TOML table")
return document


def toolchain_channel(repo_root: Path) -> str | None:
"""Return the rustup channel declared by rust-toolchain files, if any."""
toml_path = repo_root / "rust-toolchain.toml"
if toml_path.is_file() and not toml_path.is_symlink():
channel = _nested(read_toml(toml_path), "toolchain.channel")
if isinstance(channel, str) and CHANNEL_RE.fullmatch(channel):
return channel
legacy = repo_root / "rust-toolchain"
if legacy.is_file() and not legacy.is_symlink():
channel = legacy.read_text(encoding="utf-8").strip().splitlines()[0].strip()
if CHANNEL_RE.fullmatch(channel):
return channel
return None


def declared_rust_version(repo_root: Path) -> str | None:
"""Return package or workspace rust-version from the root Cargo.toml."""
manifest = repo_root / "Cargo.toml"
if not manifest.is_file() or manifest.is_symlink():
return None
document = read_toml(manifest)
for path in ("package.rust-version", "workspace.package.rust-version"):
value = _nested(document, path)
if isinstance(value, str) and value.strip():
return value.strip()
return None


def rustup_channel(repo_root: Path) -> str | None:
"""Choose the rustup toolchain the coverage image must install."""
channel = toolchain_channel(repo_root)
if channel is not None:
return channel
rust_version = declared_rust_version(repo_root)
if rust_version is None:
return None
parsed = parse_rust_version(rust_version)
if parsed is None or parsed <= DEBIAN_RUSTC:
return None
return rust_version


def _bounded_member_path(member: str) -> PurePosixPath:
"""Reject absolute or parent-directory workspace member paths."""
relative = PurePosixPath(member)
if relative.is_absolute() or ".." in relative.parts:
raise ValueError(f"workspace member is not a bounded path: {member}")
return relative


def expand_workspace_member(repo_root: Path, member: str) -> list[str]:
"""Expand one workspace member or a single trailing ``dir/*`` glob."""
if any(marker in member for marker in ("?", "[", "**")):
raise ValueError(f"unsupported workspace member glob: {member}")
if member.endswith("/*"):
parent = _bounded_member_path(member[:-2])
directory = repo_root / parent
if directory.is_symlink() or not directory.is_dir():
return []
paths: list[str] = []
for child in sorted(directory.iterdir()):
if child.is_symlink() or not child.is_dir():
continue
manifest = child / "Cargo.toml"
if manifest.is_file() and not manifest.is_symlink():
paths.append(f"{parent.as_posix()}/{child.name}/Cargo.toml")
return paths
if "*" in member:
raise ValueError(f"unsupported workspace member glob: {member}")
relative = _bounded_member_path(member)
member_manifest = f"{relative.as_posix()}/Cargo.toml"
candidate = repo_root / member_manifest
if candidate.is_file() and not candidate.is_symlink():
return [member_manifest]
return []


def workspace_member_manifests(repo_root: Path) -> list[str]:
"""Return bounded workspace member Cargo.toml paths from the root manifest."""
manifest = repo_root / "Cargo.toml"
if not manifest.is_file() or manifest.is_symlink():
return []
members = _nested(read_toml(manifest), "workspace.members")
if not isinstance(members, list):
return []
paths: list[str] = []
for member in members:
if not isinstance(member, str):
continue
paths.extend(expand_workspace_member(repo_root, member))
return list(dict.fromkeys(paths))


def tracked_paths(repo_root: Path) -> set[str]:
"""Return tracked repository paths from the materialized merge tree."""
listed = _git(repo_root, "ls-files", "-z").split(b"\0")
paths: set[str] = set()
for raw in listed:
if not raw:
continue
path = raw.decode("utf-8", errors="surrogateescape")
candidate = PurePosixPath(path)
if not candidate.is_absolute() and ".." not in candidate.parts:
paths.add(path)
return paths


def tracked_rust_inputs(repo_root: Path) -> list[str]:
"""List root and workspace Rust manifests that may enter the image context."""
if not (repo_root / "Cargo.toml").is_file():
return []
tracked = tracked_paths(repo_root)
paths = [name for name in RUST_INPUT_NAMES if name in tracked]
paths.extend(member for member in workspace_member_manifests(repo_root) if member in tracked)
return list(dict.fromkeys(paths))


def copy_bounded_file(repo_root: Path, relative: str, output_dir: Path) -> None:
"""Copy one regular, non-symlink repository file into the build context."""
source = repo_root / relative
if source.is_symlink() or not source.is_file():
raise ValueError(f"refusing to materialize non-regular Rust input: {relative}")
destination = output_dir / relative
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, destination, follow_symlinks=False)
destination.chmod(0o444)


def materialize(repo_root: Path, output_dir: Path) -> dict[str, Any]:
"""Write bounded Rust toolchain inputs and a machine-readable manifest."""
repo_root = repo_root.resolve()
output_dir.mkdir(parents=True, exist_ok=True)
inputs = tracked_rust_inputs(repo_root)
for relative in inputs:
copy_bounded_file(repo_root, relative, output_dir)
payload = {
"rustup_channel": rustup_channel(repo_root) if inputs else None,
"has_lock": "Cargo.lock" in inputs,
"has_manifest": "Cargo.toml" in inputs,
"inputs": inputs,
}
manifest = output_dir / "manifest.json"
manifest.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
manifest.chmod(0o444)
return payload


def main(argv: list[str] | None = None) -> int:
"""Copy Rust coverage inputs from the merge tree into the image build context."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--repo-root", type=Path, required=True)
parser.add_argument("--output-dir", type=Path, required=True)
args = parser.parse_args(argv)
try:
payload = materialize(args.repo_root, args.output_dir)
except (OSError, RuntimeError, ValueError, tomllib.TOMLDecodeError) as exc:
parser.error(str(exc))
print(json.dumps(payload, sort_keys=True))
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading