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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion adapters/vllm_omni/release_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,7 @@ def audit_for_knowledge(
) -> dict[str, Any]:
"""Knowledge-service plugin entry (repo-neutral ``ReleaseAuditor`` contract):
generated-baseline audit using this adapter's baseline and manifest."""
return audit_release(
data = audit_release(
upstream_repo=upstream_repo,
from_ref=from_ref,
to_ref=to_ref,
Expand All @@ -755,6 +755,8 @@ def audit_for_knowledge(
project_root=project_root,
baseline_source="generated",
).data
# which adapter file the baseline companion PR updates (relative to this adapter)
return {**data, "baseline_file": "release_baseline.yaml"}


def render_summary(report: ReleaseAuditReport) -> str:
Expand Down
7 changes: 7 additions & 0 deletions doc/architecture/SPEC/kb_service.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,10 @@ issue(同一报告只开一个),标签不存在时不带标签重试,绝
标签,进入人工队列;发布器拒绝在其上执行 `post_verdict`/`enqueue`(ready 与合并只能由人完成),且拒绝白名单外路径
(`knowledge/`、`.github/`、`src/`、`tools/`)。知识变更集处于 `companion_pending`(规则保持 active);配套 PR 合并后,调度器
在当前 main 上重建它(外部引用已清零 → 正常过门与发布);配套 PR 被关闭则知识变更转人工,且不计入熔断。

## 2026-09-29 巡检中的发版审计与基线配套 PR
配置了 `release.auditor` 的仓库,调度器在每次巡检前以 generated-baseline 模式运行审计插件(上游 SHA 对未变的回退巡检
跳过):知识文档问题按页面转为巡检生成器的提示;审计失败每次巡检只转人工一次,巡检照常进行(无提示)。reconciliation
(提交的适配器基线落后于审计的上游)不阻塞巡检,而是起草一个配套 PR:插件以 `baseline_file`(相对适配器目录)指明基线
文件,只替换其中的 `upstream:` 与 `inventories:` 块(其余内容与注释保留),经 `open_companion_pr` 以 draft 发布并转人工;
每次巡检最多一个。巡检汇总列出 reconciliation 数量。
14 changes: 10 additions & 4 deletions src/infermatrix_copilot/kb_service/companion.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,16 @@ def publish_companion(rt, lifecycle, companion_id: str) -> str:
issue_once(rt, lifecycle.repo, changeset, "open_companion_pr", {
"changeset_id": companion_id, "base_sha": data["base_sha"],
"branch": f"kb/{lifecycle.repo}/{companion_id}", "files": data["files"], "deleted": [],
"title": f"knowledge({lifecycle.repo}): update references for {changeset['detail']['for_changeset']}",
"body": ("Companion to a knowledge change that retires or replaces rules cited here "
f"(`{changeset['detail']['for_changeset']}`). Drafted by the Copilot knowledge service; "
"review, mark ready and merge it by hand. The knowledge change is rebuilt once this merges."),
"title": (f"knowledge({lifecycle.repo}): update references for {changeset['detail']['for_changeset']}"
if changeset["detail"].get("for_changeset")
else f"adapter({lifecycle.repo}): update the release baseline "
f"({changeset['detail'].get('baseline_for', '')})"),
"body": (("Companion to a knowledge change that retires or replaces rules cited here "
f"(`{changeset['detail']['for_changeset']}`). The knowledge change is rebuilt once this merges.")
if changeset["detail"].get("for_changeset")
else "The committed adapter baseline lags the audited upstream release (reconciliation "
"findings of the release audit); this updates its upstream pin and inventories.")
+ " Drafted by the Copilot knowledge service; review, mark ready and merge it by hand.",
})
rt.ledger.update_changeset(companion_id, status="pr_requested")
return "pr_requested"
2 changes: 2 additions & 0 deletions src/infermatrix_copilot/kb_service/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ def summary(rt, lifecycle, report: dict) -> tuple[str, str]:
f"({', '.join(f'{k}: {v}' for k, v in sorted(statuses.items())) or 'none'}) |",
f"| operations | {', '.join(f'{k}: {v}' for k, v in sorted(ops.items())) or 'none'} |",
f"| waiting for people | {len(rt.ledger.human_queue(lifecycle.repo))} |",
f"| release audit reconciliation findings | "
f"{'not run' if report.get('reconciliation') is None else report['reconciliation']} |",
f"| model calls | {calls} ({tokens['input']} in / {tokens['output']} out tokens, {seconds:.0f}s) |",
]
if report.get("breaker"):
Expand Down
25 changes: 23 additions & 2 deletions src/infermatrix_copilot/kb_service/scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .report import flush_reports
from .companion import publish_companion
from .external import poll_external
from .sweep_audit import audit_sweep, stage_baseline_companion
from .runtime import collect_events, publish, run_intake
from .sweep import UpstreamRepo, detect_release, run_sweep

Expand Down Expand Up @@ -129,7 +130,11 @@ def _repo_tick(self, lifecycle) -> None:
status = publish(rt, lifecycle, changeset["id"])
self._record(lifecycle.repo, "published", changeset=changeset["id"], status=status)
for changeset in rt.ledger.changesets(lifecycle.repo, ("companion_staged",)):
status = publish_companion(rt, lifecycle, changeset["id"])
try: # a companion that cannot be published yet never blocks the rest of the tick
status = publish_companion(rt, lifecycle, changeset["id"])
except Exception as exc:
self._record(lifecycle.repo, "error", error=f"companion {changeset['id']}: {exc!r}")
continue
self._record(lifecycle.repo, "published", changeset=changeset["id"], status=status)
if self._due(f"intake:{lifecycle.repo}", self.intake_every):
new = collect_events(rt, lifecycle)
Expand All @@ -141,7 +146,23 @@ def _repo_tick(self, lifecycle) -> None:
upstream = UpstreamRepo(rt.state_dir / "upstream" / f"{lifecycle.repo}.git", lifecycle.full_name)
sweep = detect_release(rt, lifecycle, upstream)
if sweep is not None:
report = run_sweep(rt, lifecycle, rt.lease_owner, sweep, upstream)
# the release audit feeds the sweep hints; baseline drift becomes a
# companion PR that runs in parallel and never blocks the sweep
base_sha = rt.knowledge.fetch() # one revision for the audit and the sweep
hints, audit = audit_sweep(rt, lifecycle, sweep, upstream, base_sha)
try: # the baseline companion never blocks the sweep
stage_baseline_companion(rt, lifecycle, rt.lease_owner, sweep, audit, base_sha)
except Exception as exc:
self._record(lifecycle.repo, "error", error=f"baseline companion: {exc!r}",
trace=traceback.format_exc()[-2000:])
key = f"baseline_companion_failed:{sweep.get('tag')}:{sweep.get('to_sha')}"
if not rt.ledger.get_cursor(lifecycle.repo, key):
rt.ledger.set_cursor(lifecycle.repo, key, "1")
rt.ledger.enqueue_human(lifecycle.repo,
f"could not draft the adapter baseline update: {exc}")
report = run_sweep(rt, lifecycle, rt.lease_owner, sweep, upstream, audit_hints=hints,
reconciliation=len(audit.reconciliation) if audit is not None else None,
base_sha=base_sha)
for changeset_id in report["changesets"]:
publish(rt, lifecycle, changeset_id)

Expand Down
11 changes: 11 additions & 0 deletions src/infermatrix_copilot/kb_service/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,17 @@ def show(self, rev: str, path: str) -> str | None:
except SourceError:
return None

def export(self, rev: str, dest: str | Path) -> Path:
"""Materialize the tree at ``rev`` under ``dest`` (no checkout involved)."""
import io
import tarfile

dest = Path(dest)
dest.mkdir(parents=True, exist_ok=True)
with tarfile.open(fileobj=io.BytesIO(self._git("archive", "--format=tar", rev))) as tar:
tar.extractall(dest, filter="data")
return dest

def raw_manifest(self, old: str, new: str) -> list[dict]:
"""Every changed path as a kb-gate manifest entry (renames are D + A),
the exact form the verifier recomputes."""
Expand Down
12 changes: 7 additions & 5 deletions src/infermatrix_copilot/kb_service/sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,8 @@ def _progress(rt, lifecycle, sweep: dict) -> dict:


def run_sweep(rt, lifecycle, owner: str, sweep: dict, upstream: UpstreamRepo,
audit_hints: dict[str, list[dict]] | None = None) -> dict:
audit_hints: dict[str, list[dict]] | None = None, reconciliation: int | None = None,
base_sha: str | None = None) -> dict:
Comment on lines 279 to +281

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Run the audit from the playbook sweep path

The new audit inputs remain optional here, so the documented playbooks/kb-sweep.yaml path still reaches engine/steps/knowledge.py::sweep, which calls run_sweep(...) without running audit_sweep or staging the baseline companion. Consequently, invoking kb run rather than the scheduler silently performs a release sweep with no audit hints, no reconciliation report, and no baseline PR; move the audit orchestration into the shared sweep path or update every entry point.

Useful? React with 👍 / 👎.

"""Traced: every model call and decision of the sweep carries its release."""
from ..trace_store import trace_context

Expand All @@ -289,7 +290,7 @@ def run_sweep(rt, lifecycle, owner: str, sweep: dict, upstream: UpstreamRepo,
run_id = sweep_run_id(lifecycle.repo, sweep, progress)
with trace_context(playbook="kb-sweep", repo=lifecycle.repo, release=str(sweep.get("tag") or ""),
run_id=run_id):
report = _run_sweep(rt, lifecycle, owner, sweep, upstream, audit_hints)
report = _run_sweep(rt, lifecycle, owner, sweep, upstream, audit_hints, reconciliation, base_sha)
report["run_id"] = run_id
return report

Expand All @@ -304,7 +305,8 @@ def draft_key_for_page(repo: str, sweep: dict, page: str) -> str:


def _run_sweep(rt, lifecycle, owner: str, sweep: dict, upstream: UpstreamRepo,
audit_hints: dict[str, list[dict]] | None = None) -> dict:
audit_hints: dict[str, list[dict]] | None = None, reconciliation: int | None = None,
base_sha: str | None = None) -> dict:
from ..trace_store import accepted_key, trace_context

accepted: dict[str, str] = {} # page -> the accepted generator call's key
Expand All @@ -317,13 +319,13 @@ def _run_sweep(rt, lifecycle, owner: str, sweep: dict, upstream: UpstreamRepo,
to re-check rules against."""
from .runtime import gate_and_stage

base_sha = rt.knowledge.fetch()
base_sha = base_sha or rt.knowledge.fetch() # the revision the audit read, when there was one
base = rt.knowledge.knowledge_files(base_sha)
external = rt.knowledge.external_texts(base_sha)
release, today = sweep["tag"] or rt.release_for(lifecycle.repo), rt.today()
progress = _progress(rt, lifecycle, sweep)
report = {"sweep": sweep, "t1": structural_report(base, lifecycle.knowledge_dir),
"changesets": [], "skipped_pages": [], "failed_pages": []}
"changesets": [], "skipped_pages": [], "failed_pages": [], "reconciliation": reconciliation}
staged: list[tuple[list, object, list, str]] = [] # (ops, result, evidence, page key)
t1 = report["t1"]
if not progress["t1_done"]:
Expand Down
115 changes: 115 additions & 0 deletions src/infermatrix_copilot/kb_service/sweep_audit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""The release audit inside each sweep, and the adapter-baseline companion PR.

A repository whose adapter configures ``release.auditor`` has its plugin run
(generated-baseline mode) for the sweep's upstream SHA pair:

* knowledge-document issues become per-page hints for the sweep's generator;
* reconciliation findings (the committed adapter baseline lags the audited
upstream) do not block the sweep. Instead the service drafts the baseline
update as a companion PR: the plugin names its baseline file
(``baseline_file``, relative to the adapter) and the audit supplies the
generated values. Only the ``upstream:`` and ``inventories:`` blocks are
replaced, so everything else in the file (comments included) is kept.
People review and merge it; the sweep never waits for it.

An audit that fails goes to people once per sweep; the sweep runs without hints.
"""

from __future__ import annotations

import tempfile

import yaml

from ..knowledge_service.release_audit import ReleaseAuditPluginError, run_release_audit

BLOCKS = ("upstream", "inventories")


def audit_sweep(rt, lifecycle, sweep: dict, upstream, base_sha: str) -> tuple[dict[str, list[dict]], object | None]:
"""(hints per knowledge-relative page, the audit result or None). The audit
reads ``base_sha`` of the knowledge repository, materialized for it: the
same revision the sweep then runs on (never a checkout or snapshot that
may lag main)."""
if not lifecycle.release.auditor or lifecycle.adapter_dir is None:
return {}, None
if not sweep.get("from_sha") or sweep.get("from_sha") == sweep.get("to_sha"):
return {}, None # nothing moved upstream (a fallback re-check)
try:
with tempfile.TemporaryDirectory(prefix="kb-audit-") as scratch:
project = rt.knowledge.export(base_sha, scratch)
result = run_release_audit(
lifecycle.adapter_dir, lifecycle.release.auditor, upstream_repo=upstream.path,
from_ref=sweep["from_sha"], to_ref=sweep["to_sha"], knowledge_root=project / "knowledge",
project_root=project)
except (ReleaseAuditPluginError, Exception) as exc: # noqa: BLE001 - never blocks the sweep
key = f"audit_failed:{sweep.get('tag')}:{sweep.get('to_sha')}"
if not rt.ledger.get_cursor(lifecycle.repo, key):
rt.ledger.set_cursor(lifecycle.repo, key, "1")
rt.ledger.enqueue_human(lifecycle.repo, f"release audit for {sweep.get('tag')} failed: {exc}")
return {}, None
hints: dict[str, list[dict]] = {}
for issue in result.issues:
page = str(issue.get("document") or "").removeprefix("knowledge/")
if page:
hints.setdefault(page, []).append(issue)
Comment on lines +51 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Escalate audit issues with no sweepable page

Audit issues are only converted to hints, but some valid enforced issues cannot be consumed by the sweep: owner_document_missing may omit document entirely, or name a document that is absent from the fetched tree. The former is dropped here and the latter is keyed to a path that rule_pages(base, ...) never iterates, so the sweep completes and advances its baseline without fixing or notifying people about the failed audit condition. Unmappable or nonexistent-page issues should be explicitly escalated.

Useful? React with 👍 / 👎.

return hints, result


def _replace_block(text: str, key: str, value) -> str:
"""Replace one top-level YAML block. It runs to the next top-level key; a
column-0 comment inside it belongs to it, but comments and blank lines
right before the next key belong to that key and are kept."""
dumped = yaml.safe_dump({key: value}, sort_keys=False, default_flow_style=False)
lines = text.splitlines(keepends=True)
start = next((i for i, line in enumerate(lines) if line.startswith(f"{key}:")), None)
if start is None:
return text.rstrip("\n") + "\n\n" + dumped
end = next((i for i in range(start + 1, len(lines))
if lines[i][:1] not in (" ", "\t", "#", "\n", "\r", "")), len(lines))
while end - 1 > start and (not lines[end - 1].strip() or lines[end - 1].startswith("#")):
end -= 1
tail = "".join(lines[end:])
return "".join(lines[:start]) + dumped + ("\n" if tail and not tail.startswith("\n") else "") + tail


def baseline_text(committed: str, generated: dict) -> str:
text = committed
for key in BLOCKS:
if key in generated:
text = _replace_block(text, key, generated[key])
return text


def stage_baseline_companion(rt, lifecycle, owner: str, sweep: dict, result, base_sha: str) -> str | None:
"""Draft the adapter-baseline update once per sweep; None when not needed."""
from .companion import publish_companion

if result is None or not result.reconciliation or lifecycle.adapter_dir is None:
return None
baseline_file = str(result.data.get("baseline_file") or "")
if not baseline_file or ".." in baseline_file.split("/") or baseline_file.startswith("/"):
return None
key = f"baseline:{sweep.get('tag')}:{sweep.get('to_sha')}"
for changeset in rt.ledger.changesets(lifecycle.repo, ("companion_staged", "pr_requested", "companion_open",
"merged", "closed", "shadow_recorded")):
if changeset["kind"] == "companion" and changeset["detail"].get("baseline_for") == key:
return None # already drafted for this sweep
path = f"adapters/{lifecycle.adapter_dir.name}/{baseline_file}"
main = base_sha # the revision the audit and the sweep use
committed = rt.knowledge.show(main, path)
if committed is None:
return None
updated = baseline_text(committed, result.generated_baseline)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle reconciliation that baseline blocks cannot fix

When the audit reports any reconciliation, this always creates a PR that changes only upstream and inventories. The vLLM-Omni auditor also classifies unmatched_path, suspicious_path_route, stale_pin, and other routing/policy findings as reconciliation, none of which this edit resolves; in particular, a changed unmatched path can disappear from future audits once the sweep baseline advances. These findings need separate remediation or escalation rather than being treated as fixed by the baseline companion.

Useful? React with 👍 / 👎.

if updated == committed:
return None
companion_id = rt.ledger.new_changeset_id(lifecycle.repo, "companion")
rt.save_changeset_files(companion_id, {"base_sha": main, "files": {path: updated}, "deleted": [],
"evidence": []})
rt.ledger.stage_intake(owner, lifecycle.repo, companion_id, kind="companion", status="companion_staged",
verdicts=[], drafted_events=[],
human_reason=f"review and merge the adapter baseline update for {sweep.get('tag')}",
detail={"for_changeset": "", "baseline_for": key, "base_sha": main,
"reconciliation": list(result.reconciliation)[:50]})
publish_companion(rt, lifecycle, companion_id)
return companion_id
Loading
Loading