Skip to content

Test - #571

Merged
plind-junior merged 226 commits into
mainfrom
test
Jul 30, 2026
Merged

Test#571
plind-junior merged 226 commits into
mainfrom
test

Conversation

@plind-junior

@plind-junior plind-junior commented Jul 28, 2026

Copy link
Copy Markdown
Member

What changed

Why

What might break

VEP

Tests

  • make check passes locally (lint + mypy + pytest)
  • New / changed behaviour has a test
  • CHANGELOG.md updated under ## [Unreleased]

Summary by CodeRabbit

  • New Features
    • Added a Sessions view for browsing, filtering, and opening compiled session summaries.
    • Introduced personal catch-all KB fallback with adoption, plus hub sync and gated imports.
    • Added VouchBench season scoring and additional bench/strategy scoring tooling.
    • Added evidence/source read tooling and “wipe dead claim references” remediation.
  • Improvements
    • Enhanced retrieval with configurable strategy ranking and improved prompt-gate behavior.
    • Added reviewer identity support for RPC calls.
  • Documentation
    • Expanded competition (koth ladder), seasons, and personal fallback guidance.
  • Tests
    • Broadened coverage for CLI, web UI, retrieval, capture, sync, validation, and dead-ref workflows.

Steve-too and others added 30 commits July 7, 2026 19:40
update_page lacked the graph-integrity ref checks that put_page and
update_claim already enforce. Vault-edit approvals could land pages
citing claims deleted between propose and approve.

Co-authored-by: Cursor <cursoragent@cursor.com>
…paths

Batch CLI already calls check_approvable(); web/MCP approve() did not,
so stale vault-edit refs surfaced as ValueError instead of ProposalError.
Defense-in-depth alongside update_page ref validation.

Co-authored-by: Cursor <cursoragent@cursor.com>
approve() now runs _payload_block_reason before delete, so the error
message uses referenced by instead of still referenced by.

Co-authored-by: Cursor <cursoragent@cursor.com>
citation_summary called list_sources(), which skips unreadable meta
via _load_or_skip. Claims citing those sources were misreported as
broken_citation in kb.stats / vouch stats — lint already treats the
source as present (unreadable_source finding instead).

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
walk approved claims grouped by shared entity, flag same-topic pairs
that disagree in polarity, and file a pending `contradicts` relation
proposal per surviving pair via proposals.propose_relation. read-only
and advisory: the scanner never sets ClaimStatus.CONTESTED, never
writes a Relation, and never touches Claim.contradicts — those only
land once a human runs `vouch approve` (or the pre-existing manual
lifecycle.contradict). scoring/grouping lives in the new
src/vouch/contradictions.py, not storage.py.

`vouch contradict-scan` mirrors `vouch dedup`'s cli-only shape:
--threshold, --entity, --limit, and --dry-run/--no-dry-run (default
dry-run). a pair already cross-linked via Claim.contradicts, joined
by an approved contradicts edge, or with a pending contradicts
proposal is skipped so repeat scans don't duplicate proposals.

closes #314
the fix landed in #484 without a changelog entry, and the
`## [Unreleased]` / `### Fixed` section is what the next release notes
are rolled from — a user-visible behaviour change (lint stops emitting
`stale_claim` warnings for retired claims) would otherwise ship
undocumented.
workflow_dispatch that reviews open prs and posts a verdict comment on
each. never merges, closes, arms auto-merge, or labels. reviews the diff
as data via the messages api (no checkout or execution of pr code), so no
environment gate is needed. owner-dispatched; optional prs input to scope
to specific numbers.
trust-gate.yml, auto-merge.yml, and comment-command.yml built the
changed-file list from `gh pr view --json files`, which is backed by
the GraphQL PullRequestFile type and carries no previous-filename
field. When GitHub detects a rename, only the new path appears there,
so `git mv src/vouch/http_server.py src/vouch/web_server.py` (plus any
edit inside it) made the changed core file invisible to pr_bot's exact-
match CORE_GLOBS check: trust-gate reported no core paths touched for
an untrusted author, and the PR was eligible for auto-merge arming as
klass=code instead of being permanently blocked as klass=core.

switch all three workflows to the REST pulls/{n}/files endpoint, which
does populate previous_filename on renamed entries, and add a
`changed-files` pr_bot subcommand that flattens that payload into both
the new and previous filename so a rename can no longer drop a core
path off the radar.

Fixes #505
…lassification-bypass

# Conflicts:
#	.github/workflows/comment-command.yml
matches house style per CodeRabbit review on #506.
parse_codex_transcript only read session_meta, which carries cwd, branch and
timestamps but not the model — codex puts the turn model on the turn_context
record. so the normalized transcript's session model was always none for
codex, while the claude parser fills it from the assistant message. read the
model off turn_context (first non-empty wins) so both agents expose it.
ground_prompt gated its items on isinstance(pack, ContextPack), but
build_context_pack returns a model_dump'd dict in production, so the guard was
always false and grounding always returned "the knowledge base has nothing on
this topic yet." — even when the kb had matching claims. dual-solve therefore
never fed the code engines any kb context.

read items from the dict (tolerating a ContextPack object too), mirroring
synthesize's grounding. the existing test monkeypatched build_context_pack to
return a ContextPack, so it exercised the dead branch and masked the bug; add a
regression that drives the real, unmocked pipeline.
the gate job publishes the required `coderabbit-approved` commit status
via POST /repos/{repo}/statuses/{sha}. for a pr opened from a fork the
GITHUB_TOKEN can't write a status onto the fork's head sha, so the api
returns 403 "resource not accessible by integration". that failed the
whole job and put a red check on every external contribution — even when
the review verdict was `approved`.

the unset status already blocks native auto-merge on its own (the ruleset
keeps waiting for it), so failing this infra job adds only noise for
outside contributors. treat a 403 on a fork head as expected: warn and
succeed. same-repo prs still hard-fail if the publish fails.
phase 3 of global vouch. `vouch hub init-personal` creates + registers
a personal kb at ~/.local/share/vouch/personal (XDG_DATA_HOME honoured,
VOUCH_PERSONAL_KB overrides). with its own config's
personal.fallback_capture flag on (opt-in: one question on a terminal
at `install-mcp --global`, or --personal-fallback / `vouch hub
fallback on`), sessions in folders WITHOUT a project kb capture into
it — origin folder stamped on every captured source — and recall reads
it back from the same folders. the session banner announces the
routing; a guard refusal (discovery landing on a personal kb from
below, the hijack shape) never falls through to fallback.

`vouch adopt`, run inside a project that now has its own kb, drains
those captures home THROUGH the project's review gate: sources copy
byte-identically (content-addressed ids survive), each live personal
claim re-proposes against the copied source, receipts re-verify
mechanically, and the project's own review config decides durability.
idempotent; --retire archives the personal copies; both kbs log a
kb.adopt event carrying the other side's id.
the "a folder without a kb never captures anywhere" wording gains its
"by default" qualifier now that the opt-in personal fallback exists;
both readmes explain the opt-in, the origin stamping, the banner
announcement, and that adoption re-verifies receipts through the
project kb's own review gate.
adversarial review of the phase-3 diff confirmed 12 findings (6 refuted).
the blocker: `adopt --retire` archived personal claims that had only
landed PENDING in the project, so rejecting or expiring the proposal
left the knowledge live in neither kb with no unarchive path. retire now
covers exactly the claims that became durable.

the rest:

- re-running adopt filed a duplicate pending proposal per pass under a
  human-only gate (`_already_durable` never saw the queue) — the guard
  now checks pending payload ids too, and --dry-run predicts against the
  project's real gate instead of assuming receipts auto-approve.
- fallback recall reads the whole personal kb, which is what a catch-all
  is — but the injected digest called it "knowledge for this repo". the
  digest header, the prompt-hook block, the banner, the opt-in question
  and both readmes now say it is one store shared by every kb-less
  folder, so recall there can surface knowledge captured elsewhere.
- session rollups filed by the fallback carried no origin: they now
  record the folder like captured sources do, and adopt reports the ones
  still pending in the personal kb instead of leaving them silent.
- a stale personal registry row could shadow a live one and switch
  fallback off; live rows now win and init-personal retires rows
  pointing elsewhere.
- OSError from the personal-kb bootstrap escaped as a traceback and made
  a successful --global install exit 1; both entry points now report a
  remedy, and the install warns without failing.
- `vouch status` in a kb-less folder said only "run vouch init" while
  the hook plane was capturing to the personal kb; it now names it.
- set_personal_fallback took the kb's cross-process lock like identity
  minting does, instead of a lock-free read-modify-write of config.yaml.
retire covers only claims that landed durable, adopt is idempotent
against the pending queue too, dry-run predicts against the real gate,
session rollups are reported rather than moved, and the personal kb is
described as the one shared store it is.
ci(coderabbit-gate): tolerate status-publish 403 on fork prs
…no-session-id

fix(capture): read session id from stdin payload in finalize-all

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/vouch/web/console.py (1)

190-201: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Cache headers computed from untrusted rel, not from the actual served candidate.

When a path-traversal attempt resets candidate to index (Line 198), the response headers are still chosen via _cache_headers(rel) using the raw, untrusted rel. A request like /assets/../../etc/passwd normalizes to rel = "assets/../../etc/passwd", which still starts with "assets/", so the SPA shell gets served with _ASSET_CACHE (immutable, 1-year) instead of _SHELL_CACHE. This defeats the exact "must always revalidate" guarantee this diff introduces — a browser hitting that specific deep link would cache the stale shell for a year.

Base the header choice on what is actually served (candidate), not on the raw request path.

🐛 Proposed fix
             if candidate.is_file():
-                return FileResponse(candidate, headers=_cache_headers(rel))
+                headers = _SHELL_CACHE if candidate == index else _cache_headers(rel)
+                return FileResponse(candidate, headers=headers)
         return FileResponse(index, headers=_SHELL_CACHE)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/web/console.py` around lines 190 - 201, Update the _spa response
flow so cache headers are selected from the file actually served, not the
untrusted rel value. When traversal validation falls back to index, or when
serving the shell, use _SHELL_CACHE; use _cache_headers only for a validated
asset candidate returned by FileResponse.
🧹 Nitpick comments (9)
src/vouch/cli.py (1)

1864-1872: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Bound the new numeric options.

--max-claims 0 / negative values flow straight into extract_mod.ingest_source; click.IntRange rejects them at parse time with a clear message.

♻️ Proposed tweak
-    "--max-claims", type=int, default=None,
+    "--max-claims", type=click.IntRange(min=1), default=None,
@@
-    "--budget-chars", type=int, default=None,
+    "--budget-chars", type=click.IntRange(min=1), default=None,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/cli.py` around lines 1864 - 1872, Update the Click option
definitions for max_claims and budget_chars to use click.IntRange validation,
rejecting zero and negative values during argument parsing while preserving None
as the unset default.
contrib/strategies/relevance_guard.py (1)

63-66: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

CONFLICT_PENALTY / POSSESSIVE_PENALTY are unused; the real penalties are inline literals.

rank() uses hardcoded 10.0, 6.0, and 4.0 (Lines 104, 122, 124), so these two module constants are dead and actively misleading to anyone tuning this file. Either wire them in or drop them.

♻️ Suggested cleanup
 SCORE_W = 0.7
 OVERLAP_W = 0.3
-CONFLICT_PENALTY = 3.0
-POSSESSIVE_PENALTY = 4.0
+DANGER_SCALE = 4.0
+INSTRUCTION_PENALTY = 10.0
+SUSPECT_PENALTY = 6.0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@contrib/strategies/relevance_guard.py` around lines 63 - 66, Update rank() to
use the existing CONFLICT_PENALTY and POSSESSIVE_PENALTY constants wherever the
corresponding inline penalty literals are applied, or remove those constants if
they are not intended to control the calculations; eliminate the misleading
unused definitions while preserving the current penalty behavior.
src/vouch/chatgpt_import.py (1)

460-475: 🗄️ Data Integrity & Integration | 🔵 Trivial | 🏗️ Heavy lift

The refresh path mutates a proposal payload directly, sidestepping propose_page's validation.

New conversations go through propose_page (page-kind validation, scope, slug handling); the re-import path writes title/body/sources straight into the stored payload with store.update_proposal. Any validation propose_page gains later won't apply to refreshes, and proposed_at is left stale. Prefer a proposals.* helper for "refresh a pending page proposal" so both surfaces share one code path.

As per path instructions, "MCP/JSONL/CLI share the same storage + proposals + audit code path … avoid introducing divergence."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/chatgpt_import.py` around lines 460 - 475, Replace the direct
payload mutation and store.update_proposal flow in the existing-proposal branch
with the shared proposals helper used to refresh a pending page proposal,
routing title, body, and source updates through propose_page or its equivalent.
Preserve the existing audit event and actor/object metadata while ensuring the
shared path performs page-kind, scope, slug, and proposed_at handling
consistently with new imports.

Source: Path instructions

src/vouch/context.py (1)

330-356: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

config.yaml is now parsed up to four times per query.

_retrieval_config re-reads and re-parses the file on every call, and _maybe_strategy can call it twice (params arm, then dotted arm) on top of the existing _configured_recency / _configured_rerank / _configured_backend parses. Retrieval is the hot path; consider a single per-store read (e.g. functools.lru_cache keyed on path+mtime, or threading one parsed mapping through _retrieve) and have _configured_recency/_configured_rerank consume _retrieval_config too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/context.py` around lines 330 - 356, The retrieval configuration is
reparsed multiple times per query, including separate reads in strategy and
existing retrieval option helpers. Refactor the retrieval flow around
_retrieval_config, _maybe_strategy, _configured_recency, _configured_rerank, and
_configured_backend to perform one per-store config read and reuse the parsed
retrieval mapping, either by threading it through _retrieve or using a
path/mtime-aware cache. Ensure all retrieval option helpers consume that shared
result without changing configuration behavior.
src/vouch/capture.py (1)

896-911: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

finalize_all_except's docstring still describes the old receipt-only drain.

Line 928 documents auto_approved as "count of pending receipt-verified claims drained on the way", but _drain_approval_backlog now drains whatever the configured gate allows (pages, entities, relations included under trusted-agent). Worth updating so the reported number isn't misread.

Also applies to: 941-941

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/capture.py` around lines 896 - 911, Update the auto_approved
documentation in finalize_all_except to describe the count returned by
_drain_approval_backlog as all pending proposals approved under the configured
gate, including claims, pages, entities, and relations where applicable, rather
than only receipt-verified claims.
src/vouch/proposals.py (1)

527-538: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use store.config_path instead of re-deriving the filename.

KBStore.config_path already resolves CONFIG_FILENAME; hardcoding "config.yaml" here is a second source of truth.

♻️ Proposed tweak
-        loaded = yaml.safe_load(
-            (store.kb_dir / "config.yaml").read_text(encoding="utf-8")
-        )
+        loaded = yaml.safe_load(store.config_path.read_text(encoding="utf-8"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/proposals.py` around lines 527 - 538, Update the YAML loading logic
in the review configuration path to read from store.config_path instead of
constructing store.kb_dir / "config.yaml". Preserve the existing parsing, error
handling, and review normalization behavior.
webapp/src/views/ChatView.tsx (1)

120-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two hand-maintained copies of the drawer-kind allowlist.

This Set and DRAWER_KINDS in webapp/src/views/BrowseView.tsx (Line 16) both enumerate the same kinds as OpenKind; a future kind added to OpenKind will silently stay unclickable in one of them. Exporting one canonical list next to OpenKind in ArtifactDrawer.tsx and deriving both call sites from it keeps them in lockstep.

♻️ Sketch
// ArtifactDrawer.tsx
export const OPEN_KINDS = ['claim', 'page', 'entity', 'relation', 'evidence', 'source'] as const
export type OpenKind = (typeof OPEN_KINDS)[number]
-const DRAWER_KINDS = new Set(['claim', 'page', 'entity', 'relation', 'evidence', 'source'])
+const DRAWER_KINDS: ReadonlySet<string> = new Set(OPEN_KINDS)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/views/ChatView.tsx` at line 120, Replace the local DRAWER_KINDS
Set in ChatView.tsx and the corresponding allowlist in BrowseView.tsx with a
canonical OPEN_KINDS list exported from ArtifactDrawer.tsx. Derive OpenKind from
OPEN_KINDS there, and update both call sites to use the shared list while
preserving their existing membership-check behavior.
webapp/src/components/ArtifactDrawer.tsx (1)

79-96: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

A 10k-event audit fetch to render one event row.

Expanding a single provenance edge pulls the whole tail; it's cached per endpoint, but the first expansion pays the full transfer and parse on a KB with a long history. If kb.audit accepts filtering (e.g. by object id or a since cursor), prefer that; otherwise consider a smaller tail with a "load more" fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/ArtifactDrawer.tsx` around lines 79 - 96, Update
EventDetail’s audit query to avoid fetching AUDIT_TAIL events when resolving a
single provenance edge: use the kb.audit filtering capability by event/object id
or an appropriate since cursor if supported. If filtering is unavailable, reduce
the initial tail size and add a load-more path that fetches older events only
when the requested event is not found, while preserving the existing
endpoint-level query caching.
tests/test_adopt.py (1)

121-130: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting the repeated closed-gate setup into a helper.

The same three-line block (set auto_approve_on_receipt = False, pop approver_role, write back) is duplicated across four tests. A small helper would reduce duplication and centralize the "closed gate" definition if it needs to evolve.

♻️ Proposed helper extraction
+def _close_review_gate(project: KBStore) -> None:
+    cfg = yaml.safe_load(project.config_path.read_text(encoding="utf-8"))
+    cfg["review"]["auto_approve_on_receipt"] = False
+    cfg["review"].pop("approver_role", None)
+    project.config_path.write_text(yaml.safe_dump(cfg), encoding="utf-8")

Then replace each 4-line inline block with _close_review_gate(project).

Also applies to: 277-289, 357-370, 392-402

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_adopt.py` around lines 121 - 130, Extract the repeated review-gate
configuration block into a `_close_review_gate(project)` helper that disables
`auto_approve_on_receipt`, removes `approver_role`, and writes the updated
configuration. Replace the duplicated setup in
`test_adopt_respects_a_closed_gate` and the other referenced tests with calls to
this helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/scripts/promote_champion.py:
- Around line 41-47: Update the module-docstring removal logic around stripped
in the promotion flow to recognize and remove valid leading docstrings using
both triple-double-quote and triple-single-quote delimiters. Preserve the
existing behavior of removing only a leading docstring and trimming the
following newline so the champion header remains the sole provenance header.

In @.github/workflows/koth-ledger.yml:
- Around line 81-93: Validate the value assigned to mean in the engine branch
before invoking promote_champion.py: parse .challenger.mean only when it is
numeric, and skip promotion when the field is absent, null, or malformed. Wrap
the existing winner check and promotion call in a non-empty mean guard,
preserving the remaining PR iteration steps.

In `@CHANGELOG.md`:
- Line 150: Merge the duplicate Fixed and Added headings in CHANGELOG.md into
the existing corresponding sections under [Unreleased]. Move the entries from
the newly added headings into those sections and remove the redundant headings
so changelog lint no longer reports repeated headings.

In `@docs/mining-on-vouch.md`:
- Around line 78-81: Update CHANGELOG.md under the [Unreleased] section to
document the user-visible automatic champion promotion and merge-threshold
ratcheting behavior described in the mining-on-vouch documentation.

In `@src/vouch/chatgpt_import.py`:
- Around line 372-383: Replace the per-call scan in _find_existing_page with a
single _page_proposals_by_session index built before the import_export
conversation loop. Use existing_pages.get(row["session_id"]) during processing,
and update the index whenever a new page proposal is filed so duplicate session
IDs within the same export still deduplicate.
- Around line 111-128: Update the ZIP handling in the import flow to enforce
_MAX_EXPORT_BYTES on the decompressed data actually read, rather than trusting
info.file_size from the archive metadata. Ensure zf.read(info) cannot
materialize more than the allowed limit, and retain the existing
ChatGPTImportError behavior for oversized entries.

In `@src/vouch/cli.py`:
- Around line 4131-4152: Update the [Unreleased] section of CHANGELOG.md to
document the new user-facing CLI commands import-chatgpt, read-evidence,
read-source, and wipe-dead-refs, matching the changelog’s existing style and
grouping.

In `@src/vouch/stats.py`:
- Around line 94-96: Lowercase the newly added comment prose in the
stats-related code: change its opening “Use” to “use” and ensure the entire
comment remains lowercase, without altering its meaning or surrounding
implementation.

In `@src/vouch/storage.py`:
- Around line 84-94: Remove the default "approver_role": "trusted-agent" entry
from the starter configuration in the initialization flow so the review gate
remains enabled by default. Keep auto_approve_on_receipt enabled as the safe
default, and update vouch init to surface the trusted-agent shortcut as an
explicit opt-in configuration rather than enabling it implicitly.

In `@tests/test_read_evidence_source.py`:
- Around line 38-41: Update the _rpc helper and the related kb.* method tests to
validate the complete JSONL response envelope: assert the echoed id and
successful result, and add failure-path assertions for ok=false with an error
field. Apply the same envelope checks to the tests referenced around lines 69-74
while preserving their existing method-specific assertions.

In `@tests/test_strategy_configured.py`:
- Around line 27-28: Update the configured-strategy tests to load the current
champion from contrib/strategies/baseline.py instead of the static
RELEVANCE_GUARD path. Adjust the affected test setup and assertions around the
referenced lines so the invariant compares against the baseline file that
promotions update, while preserving the existing test behavior.

In `@tests/test_trusted_agent_auto_approve.py`:
- Around line 38-44: Update the starter configuration defaults exercised by
test_starter_config_defaults_to_trusted_agent so review.approver_role is a
non-trusted approver and auto_approve_on_receipt does not bypass human review by
default. Preserve trusted-agent approval only when review.approver_role is
explicitly set to trusted-agent, and revise the test assertions to verify the
opt-in requirement.

In `@webapp/src/components/ArtifactDrawer.tsx`:
- Around line 235-252: Update the catch block in resolveOpen to rethrow
VouchHttpError for authentication, transport, and non-2xx failures, and continue
probing only when the caught VouchRpcError represents a missing artifact for the
current kind. Preserve the existing success callback and final toast behavior
for ids that do not resolve in any readable kind.

---

Outside diff comments:
In `@src/vouch/web/console.py`:
- Around line 190-201: Update the _spa response flow so cache headers are
selected from the file actually served, not the untrusted rel value. When
traversal validation falls back to index, or when serving the shell, use
_SHELL_CACHE; use _cache_headers only for a validated asset candidate returned
by FileResponse.

---

Nitpick comments:
In `@contrib/strategies/relevance_guard.py`:
- Around line 63-66: Update rank() to use the existing CONFLICT_PENALTY and
POSSESSIVE_PENALTY constants wherever the corresponding inline penalty literals
are applied, or remove those constants if they are not intended to control the
calculations; eliminate the misleading unused definitions while preserving the
current penalty behavior.

In `@src/vouch/capture.py`:
- Around line 896-911: Update the auto_approved documentation in
finalize_all_except to describe the count returned by _drain_approval_backlog as
all pending proposals approved under the configured gate, including claims,
pages, entities, and relations where applicable, rather than only
receipt-verified claims.

In `@src/vouch/chatgpt_import.py`:
- Around line 460-475: Replace the direct payload mutation and
store.update_proposal flow in the existing-proposal branch with the shared
proposals helper used to refresh a pending page proposal, routing title, body,
and source updates through propose_page or its equivalent. Preserve the existing
audit event and actor/object metadata while ensuring the shared path performs
page-kind, scope, slug, and proposed_at handling consistently with new imports.

In `@src/vouch/cli.py`:
- Around line 1864-1872: Update the Click option definitions for max_claims and
budget_chars to use click.IntRange validation, rejecting zero and negative
values during argument parsing while preserving None as the unset default.

In `@src/vouch/context.py`:
- Around line 330-356: The retrieval configuration is reparsed multiple times
per query, including separate reads in strategy and existing retrieval option
helpers. Refactor the retrieval flow around _retrieval_config, _maybe_strategy,
_configured_recency, _configured_rerank, and _configured_backend to perform one
per-store config read and reuse the parsed retrieval mapping, either by
threading it through _retrieve or using a path/mtime-aware cache. Ensure all
retrieval option helpers consume that shared result without changing
configuration behavior.

In `@src/vouch/proposals.py`:
- Around line 527-538: Update the YAML loading logic in the review configuration
path to read from store.config_path instead of constructing store.kb_dir /
"config.yaml". Preserve the existing parsing, error handling, and review
normalization behavior.

In `@tests/test_adopt.py`:
- Around line 121-130: Extract the repeated review-gate configuration block into
a `_close_review_gate(project)` helper that disables `auto_approve_on_receipt`,
removes `approver_role`, and writes the updated configuration. Replace the
duplicated setup in `test_adopt_respects_a_closed_gate` and the other referenced
tests with calls to this helper.

In `@webapp/src/components/ArtifactDrawer.tsx`:
- Around line 79-96: Update EventDetail’s audit query to avoid fetching
AUDIT_TAIL events when resolving a single provenance edge: use the kb.audit
filtering capability by event/object id or an appropriate since cursor if
supported. If filtering is unavailable, reduce the initial tail size and add a
load-more path that fetches older events only when the requested event is not
found, while preserving the existing endpoint-level query caching.

In `@webapp/src/views/ChatView.tsx`:
- Line 120: Replace the local DRAWER_KINDS Set in ChatView.tsx and the
corresponding allowlist in BrowseView.tsx with a canonical OPEN_KINDS list
exported from ArtifactDrawer.tsx. Derive OpenKind from OPEN_KINDS there, and
update both call sites to use the shared list while preserving their existing
membership-check behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1e6e4816-5edd-45f0-adb2-3e6e4d09619e

📥 Commits

Reviewing files that changed from the base of the PR and between 1fe8fb5 and 12be9c3.

📒 Files selected for processing (101)
  • .github/scripts/promote_champion.py
  • .github/scripts/score_strategy.py
  • .github/scripts/validate_kit.py
  • .github/workflows/koth-engine-gate.yml
  • .github/workflows/koth-ledger.yml
  • .github/workflows/pr-review-sweep.yml
  • .gitignore
  • .vouch/.gitignore
  • .vouch/audit.log.jsonl
  • .vouch/captures/1218169c-de25-48f4-bd93-70ff77d2fda2.jsonl
  • .vouch/captures/5720220c-b77c-4947-aeec-9d6ef91d70b8.jsonl
  • .vouch/captures/8a6df051-6c56-4d86-b0ed-66f6638ecfdc.jsonl
  • .vouch/captures/ec4b8842-f167-467e-a33c-dc87911c9e0e.jsonl
  • .vouch/claims/vouch-starter-reviewed-knowledge.yaml
  • .vouch/claims/vouch-uses-a-review-gated-proposal-workflow-agents-propose-c.yaml
  • .vouch/config.yaml
  • .vouch/decided/20260521-055206-7d6d92d6.yaml
  • .vouch/decided/20260707-092531-154cb163.yaml
  • .vouch/decided/20260707-093005-05345f47.yaml
  • .vouch/decided/20260707-093125-4d6972f9.yaml
  • .vouch/pages/edit-in-obsidian.md
  • .vouch/pages/reviewed-knowledge-store.md
  • .vouch/pages/session-what-are-the-non-development-related-files-and-folde.md
  • .vouch/schema_version
  • .vouch/sources/06d8519f8dcf4149d23c8a48984541b2e9365ec364e7e58192e28ed149a2c47c/content
  • .vouch/sources/06d8519f8dcf4149d23c8a48984541b2e9365ec364e7e58192e28ed149a2c47c/meta.yaml
  • .vouch/sources/67478e72acfb8fac3a059143e95c95f5cc6f7e8d4dccc05fbcea8dbccb8a4eba/content
  • .vouch/sources/67478e72acfb8fac3a059143e95c95f5cc6f7e8d4dccc05fbcea8dbccb8a4eba/meta.yaml
  • .vouch/sources/be7aec64b0fc803a33cb3d610f67ae95e636877db20231ef72440a7cbe6b69d2/content
  • .vouch/sources/be7aec64b0fc803a33cb3d610f67ae95e636877db20231ef72440a7cbe6b69d2/meta.yaml
  • CHANGELOG.md
  • README.md
  • competition/LEADERBOARD.md
  • contrib/strategies/relevance_guard.py
  • docs/koth-ladder.md
  • docs/koth-strategy-lane.md
  • docs/mining-on-vouch.md
  • src/vouch/admission.py
  • src/vouch/bench.py
  • src/vouch/capabilities.py
  • src/vouch/capture.py
  • src/vouch/chatgpt_import.py
  • src/vouch/cli.py
  • src/vouch/compile.py
  • src/vouch/config_coerce.py
  • src/vouch/context.py
  • src/vouch/extract.py
  • src/vouch/hot_memory.py
  • src/vouch/http_server.py
  • src/vouch/inbox.py
  • src/vouch/jsonl_server.py
  • src/vouch/lifecycle.py
  • src/vouch/proposals.py
  • src/vouch/recall.py
  • src/vouch/sandbox.py
  • src/vouch/server.py
  • src/vouch/session_split.py
  • src/vouch/stats.py
  • src/vouch/storage.py
  • src/vouch/strategies/configured.py
  • src/vouch/strategy.py
  • src/vouch/verify.py
  • src/vouch/volunteer_context.py
  • src/vouch/web/console.py
  • tests/test_admission.py
  • tests/test_adopt.py
  • tests/test_bench.py
  • tests/test_capture.py
  • tests/test_chatgpt_import.py
  • tests/test_compile.py
  • tests/test_config_coerce.py
  • tests/test_console.py
  • tests/test_context.py
  • tests/test_delete.py
  • tests/test_extract.py
  • tests/test_hot_memory.py
  • tests/test_http_server.py
  • tests/test_inbox.py
  • tests/test_jsonl_server.py
  • tests/test_koth_kit.py
  • tests/test_lifecycle.py
  • tests/test_promote_champion.py
  • tests/test_read_evidence_source.py
  • tests/test_recall.py
  • tests/test_receipt_auto_approve.py
  • tests/test_sandbox.py
  • tests/test_session_split.py
  • tests/test_session_transcript.py
  • tests/test_stats.py
  • tests/test_strategy_configured.py
  • tests/test_trusted_agent_auto_approve.py
  • tests/test_verify.py
  • tests/test_volunteer_context.py
  • tests/test_web.py
  • webapp/.gitignore
  • webapp/src/components/ArtifactDrawer.test.tsx
  • webapp/src/components/ArtifactDrawer.tsx
  • webapp/src/lib/types.ts
  • webapp/src/views/BrowseView.tsx
  • webapp/src/views/ChatView.test.tsx
  • webapp/src/views/ChatView.tsx
💤 Files with no reviewable changes (24)
  • .vouch/sources/be7aec64b0fc803a33cb3d610f67ae95e636877db20231ef72440a7cbe6b69d2/content
  • .vouch/captures/1218169c-de25-48f4-bd93-70ff77d2fda2.jsonl
  • .vouch/sources/67478e72acfb8fac3a059143e95c95f5cc6f7e8d4dccc05fbcea8dbccb8a4eba/content
  • .vouch/decided/20260521-055206-7d6d92d6.yaml
  • .vouch/decided/20260707-093125-4d6972f9.yaml
  • .vouch/.gitignore
  • .vouch/claims/vouch-starter-reviewed-knowledge.yaml
  • .vouch/claims/vouch-uses-a-review-gated-proposal-workflow-agents-propose-c.yaml
  • .vouch/captures/ec4b8842-f167-467e-a33c-dc87911c9e0e.jsonl
  • .vouch/sources/06d8519f8dcf4149d23c8a48984541b2e9365ec364e7e58192e28ed149a2c47c/content
  • .vouch/sources/67478e72acfb8fac3a059143e95c95f5cc6f7e8d4dccc05fbcea8dbccb8a4eba/meta.yaml
  • .vouch/decided/20260707-093005-05345f47.yaml
  • .vouch/sources/06d8519f8dcf4149d23c8a48984541b2e9365ec364e7e58192e28ed149a2c47c/meta.yaml
  • .vouch/pages/edit-in-obsidian.md
  • .vouch/schema_version
  • .vouch/audit.log.jsonl
  • .vouch/decided/20260707-092531-154cb163.yaml
  • .vouch/pages/session-what-are-the-non-development-related-files-and-folde.md
  • .vouch/pages/reviewed-knowledge-store.md
  • .vouch/config.yaml
  • .vouch/sources/be7aec64b0fc803a33cb3d610f67ae95e636877db20231ef72440a7cbe6b69d2/meta.yaml
  • .vouch/captures/8a6df051-6c56-4d86-b0ed-66f6638ecfdc.jsonl
  • .vouch/captures/5720220c-b77c-4947-aeec-9d6ef91d70b8.jsonl
  • README.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • competition/LEADERBOARD.md
  • docs/koth-strategy-lane.md
  • docs/koth-ladder.md
  • tests/test_admission.py
  • src/vouch/jsonl_server.py
  • tests/test_capture.py
  • tests/test_compile.py

Comment on lines +41 to +47
# drop the submission's own module docstring so the champion header is
# the single source of provenance
stripped = body
if stripped.startswith('"""'):
end = stripped.find('"""', 3)
if end != -1:
stripped = stripped[end + 3 :].lstrip("\n")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n --glob '*.py' "^[[:space:]]*'''" contrib/strategies tests

Repository: vouchdev/vouch

Length of output: 190


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate promote_champion.py =="
fd -a 'promote_champion.py' . || true

echo
echo "== relevant promote script =="
if [ -f .github/scripts/promote_champion.py ]; then
  cat -n .github/scripts/promote_champion.py
fi

echo
echo "== tests =="
if [ -f tests/test_promote_champion.py ]; then
  cat -n tests/test_promote_champion.py
fi

echo
echo "== occurrences of promote_champion imports/calls =="
rg -n "promote_champion|promote\\(" .github scripts tests contrib strategies 2>/dev/null || true

echo
echo "== read-only parser probe for leading module docstrings =="
python3 - <<'PY'
from ast import parse

samples = [
    '"""double"""',
    "'''single'''",
    '"""multi\nline"""',
    "'''multi\nline'''",
    '# note\n"""doc"""',
    'import sys\n"""doc"""',
]
for src in samples:
    tree = parse(src)
    print(repr(src), "=>", ast.get_docstring(tree) if hasattr(ast, "get_docstring") else getattr(tree, "get_docstring", lambda: None)())
PY

Repository: vouchdev/vouch

Length of output: 6982


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant parser probe =="
python3 - <<'PY'
import ast

samples = [
    '"""double"""',
    "'''single'''",
    '"""multi\nline"""',
    "'''multi\nline'''",
    '# note\n"""doc"""',
    'import sys\n"""doc"""',
]
for src in samples:
    tree = parse(src)
    print(repr(src), "=>", ast.get_docstring(tree))
PY

Repository: vouchdev/vouch

Length of output: 293


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Python docstring probe =="
python3 - <<'PY'
import ast

samples = [
    '"""double"""',
    "'''single'''",
    '"""multi\nline"""',
    "'''multi\nline'''",
    '# note\n"""doc"""',
    'import sys\n"""doc"""',
]
for src in samples:
    tree = ast.parse(src)
    print(repr(src), "=>", ast.get_docstring(tree))
PY

echo
echo "== behavioral probe for current promote() logic =="
python3 - <<'PY'
import ast

HEADER_PREFIX = '"""The reigning engine-lane champion (auto-promoted, do not edit).'

def promote(body: str) -> str:
    stripped = body
    if stripped.startswith('"""'):
        end = stripped.find('"""', 3)
        if end != -1:
            stripped = stripped[end + 3 :]
    new = HEADER_PREFIX + '..."'
    new += stripped
    return new

for winner_doc in ['"""double"""', "'''single'''", '"""multi\nline"""', "'''multi\nline'''"]:
    body = winner_doc + '\n\ndef rank(query, candidates, *, limit):\n    return [c.id for c in candidates]\n'
    result = promote(body)
    print("winner_doc=", repr(winner_doc))
    print(result)
    print("single header count=", result.count('"""The reigning engine-lane champion'))
    print("contains original winning docstring in output?", winner_doc[3:-3] in result[3:])
PY

Repository: vouchdev/vouch

Length of output: 1458


strip every valid module-docstring form.

A valid leading '''...''' module docstring is retained, leaving the promoted code with both the provenance header and the original submission docstring. Parse module docstrings robustly, or at least support both triple-quote delimiters.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/scripts/promote_champion.py around lines 41 - 47, Update the
module-docstring removal logic around stripped in the promotion flow to
recognize and remove valid leading docstrings using both triple-double-quote and
triple-single-quote delimiters. Preserve the existing behavior of removing only
a leading docstring and trimming the following newline so the champion header
remains the sole provenance header.

Comment on lines +81 to +93
lane=$(jq -r '.lane // "kit"' /tmp/report.json)
if [ "$lane" = "engine" ]; then
mean=$(jq -r '.challenger.mean' /tmp/report.json)
winner=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr}/files" \
--paginate \
--jq '.[] | select(.status != "removed") | .filename' \
| grep -E '^contrib/strategies/[A-Za-z0-9_]+\.py$' \
| grep -v 'baseline\.py' | head -1 || true)
if [ -n "$winner" ] && [ -f "$winner" ]; then
python3 .github/scripts/promote_champion.py \
--strategy "$winner" --pr "$pr" --mean "$mean"
fi
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Guard mean before promoting.

jq -r '.challenger.mean' yields the literal string null when the key is absent or not a number, and that value is passed straight into promote_champion.py --mean, so a malformed report ratchets the champion threshold with garbage instead of skipping.

🛡️ Proposed guard
-                mean=$(jq -r '.challenger.mean' /tmp/report.json)
+                mean=$(jq -r '(.challenger.mean | numbers) // empty' /tmp/report.json)
+                if [ -z "$mean" ]; then
+                  echo "report has no numeric challenger.mean - skipping ratchet" >&2
+                  continue
+                fi

Note continue skips the rest of the PR loop iteration; use an if [ -n "$mean" ] wrapper instead if the remaining steps in the iteration must still run.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
lane=$(jq -r '.lane // "kit"' /tmp/report.json)
if [ "$lane" = "engine" ]; then
mean=$(jq -r '.challenger.mean' /tmp/report.json)
winner=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr}/files" \
--paginate \
--jq '.[] | select(.status != "removed") | .filename' \
| grep -E '^contrib/strategies/[A-Za-z0-9_]+\.py$' \
| grep -v 'baseline\.py' | head -1 || true)
if [ -n "$winner" ] && [ -f "$winner" ]; then
python3 .github/scripts/promote_champion.py \
--strategy "$winner" --pr "$pr" --mean "$mean"
fi
fi
lane=$(jq -r '.lane // "kit"' /tmp/report.json)
if [ "$lane" = "engine" ]; then
mean=$(jq -r '(.challenger.mean | numbers) // empty' /tmp/report.json)
if [ -z "$mean" ]; then
echo "report has no numeric challenger.mean - skipping ratchet" >&2
continue
fi
winner=$(gh api "repos/${GITHUB_REPOSITORY}/pulls/${pr}/files" \
--paginate \
--jq '.[] | select(.status != "removed") | .filename' \
| grep -E '^contrib/strategies/[A-Za-z0-9_]+\.py$' \
| grep -v 'baseline\.py' | head -1 || true)
if [ -n "$winner" ] && [ -f "$winner" ]; then
python3 .github/scripts/promote_champion.py \
--strategy "$winner" --pr "$pr" --mean "$mean"
fi
fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/koth-ledger.yml around lines 81 - 93, Validate the value
assigned to mean in the engine branch before invoking promote_champion.py: parse
.challenger.mean only when it is numeric, and skip promotion when the field is
absent, null, or malformed. Wrap the existing winner check and promotion call in
a non-empty mean guard, preserving the remaining PR iteration steps.

Comment thread CHANGELOG.md
Comment thread docs/mining-on-vouch.md
Comment on lines +78 to +81
credited in the changelog. on merge, the ledger sweep promotes the
winner to reigning champion automatically — the merge threshold ratchets
up with every merged win, because the next challenger is scored against
you.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

add an Unreleased changelog entry.

Automatic champion promotion and threshold ratcheting are user-visible behavior; record them in CHANGELOG.md in this PR.

As per coding guidelines, “When adding a user-visible feature, update CHANGELOG.md under [Unreleased] in the same PR.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/mining-on-vouch.md` around lines 78 - 81, Update CHANGELOG.md under the
[Unreleased] section to document the user-visible automatic champion promotion
and merge-threshold ratcheting behavior described in the mining-on-vouch
documentation.

Source: Coding guidelines

Comment on lines +111 to +128
if is_zip:
with zipfile.ZipFile(path) as zf:
infos = [
i for i in zf.infolist()
if i.filename.rsplit("/", 1)[-1] == "conversations.json"
]
if not infos:
raise ChatGPTImportError(
f"{path.name} has no conversations.json — this doesn't "
"look like a ChatGPT data export ZIP"
)
info = infos[0]
if info.file_size > _MAX_EXPORT_BYTES:
raise ChatGPTImportError(
f"{info.filename} is too large to import "
f"({info.file_size} bytes > {_MAX_EXPORT_BYTES} byte limit)"
)
return zf.read(info)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

The ZIP size guard trusts the header-declared file_size.

info.file_size comes from the archive's own central directory, so a crafted entry can understate it and zf.read(info) will still decompress the full payload into memory. Cap the actual read instead.

🛡️ Proposed hardening
             info = infos[0]
             if info.file_size > _MAX_EXPORT_BYTES:
                 raise ChatGPTImportError(
                     f"{info.filename} is too large to import "
                     f"({info.file_size} bytes > {_MAX_EXPORT_BYTES} byte limit)"
                 )
-            return zf.read(info)
+            with zf.open(info) as fh:
+                data = fh.read(_MAX_EXPORT_BYTES + 1)
+            if len(data) > _MAX_EXPORT_BYTES:
+                raise ChatGPTImportError(
+                    f"{info.filename} is too large to import "
+                    f"(> {_MAX_EXPORT_BYTES} byte limit)"
+                )
+            return data
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if is_zip:
with zipfile.ZipFile(path) as zf:
infos = [
i for i in zf.infolist()
if i.filename.rsplit("/", 1)[-1] == "conversations.json"
]
if not infos:
raise ChatGPTImportError(
f"{path.name} has no conversations.json — this doesn't "
"look like a ChatGPT data export ZIP"
)
info = infos[0]
if info.file_size > _MAX_EXPORT_BYTES:
raise ChatGPTImportError(
f"{info.filename} is too large to import "
f"({info.file_size} bytes > {_MAX_EXPORT_BYTES} byte limit)"
)
return zf.read(info)
if is_zip:
with zipfile.ZipFile(path) as zf:
infos = [
i for i in zf.infolist()
if i.filename.rsplit("/", 1)[-1] == "conversations.json"
]
if not infos:
raise ChatGPTImportError(
f"{path.name} has no conversations.json — this doesn't "
"look like a ChatGPT data export ZIP"
)
info = infos[0]
if info.file_size > _MAX_EXPORT_BYTES:
raise ChatGPTImportError(
f"{info.filename} is too large to import "
f"({info.file_size} bytes > {_MAX_EXPORT_BYTES} byte limit)"
)
with zf.open(info) as fh:
data = fh.read(_MAX_EXPORT_BYTES + 1)
if len(data) > _MAX_EXPORT_BYTES:
raise ChatGPTImportError(
f"{info.filename} is too large to import "
f"(> {_MAX_EXPORT_BYTES} byte limit)"
)
return data
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/chatgpt_import.py` around lines 111 - 128, Update the ZIP handling
in the import flow to enforce _MAX_EXPORT_BYTES on the decompressed data
actually read, rather than trusting info.file_size from the archive metadata.
Ensure zf.read(info) cannot materialize more than the allowed limit, and retain
the existing ChatGPTImportError behavior for oversized entries.

Comment thread src/vouch/storage.py
Comment on lines +84 to 94
# auto approval is the default: the capturing agent's proposals
# self-approve through the same proposals.approve() gate (one
# audit event per artifact, duplicates rejected, protected page
# kinds and DELETE proposals still held for a reviewer). Remove
# approver_role to put every other write behind `vouch review`.
"approver_role": "trusted-agent",
# phase d — the receipt is the reviewer. When true, a claim whose
# byte-offset receipts all verify is auto-approved with no human;
# a claim that cannot quote its source is left pending, as is
# every page/entity/relation proposal. Set false to put every
# write behind `vouch review`.
# a claim that cannot quote its source is left pending. Only
# consulted when approver_role does not already clear the write.
"auto_approve_on_receipt": True,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Shipping approver_role: trusted-agent in the starter config makes the trusted-agent shortcut opt-out.

Every newly vouch init-ed KB now self-approves every non-protected write: proposals._approval_block_reason returns None for self-approval under this role, and the new proposals.auto_approve_pending drains all pending claims/pages/entities/relations on a SessionStart hook. The documented contract is that the shortcut is opt-in, so the default should leave it unset (receipt-based auto-approval alone is the safe default) and vouch init should surface the flip explicitly.

As per coding guidelines, "Do not implement SaaS/hosted mode, remove the review gate for trusted agents, …"; as per path instructions, "every kb.* write tool is review-gated with no trusted-agent shortcut except opt-in review.approver_role: trusted-agent".

🔒 Suggested default
-            # auto approval is the default: the capturing agent's proposals
-            # self-approve through the same proposals.approve() gate (one
-            # audit event per artifact, duplicates rejected, protected page
-            # kinds and DELETE proposals still held for a reviewer). Remove
-            # approver_role to put every other write behind `vouch review`.
-            "approver_role": "trusted-agent",
+            # opt in by setting `approver_role: trusted-agent` to let the
+            # capturing agent's proposals self-approve through the same
+            # proposals.approve() gate. Left unset so writes stay behind
+            # `vouch review` unless the receipt gate below clears them.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# auto approval is the default: the capturing agent's proposals
# self-approve through the same proposals.approve() gate (one
# audit event per artifact, duplicates rejected, protected page
# kinds and DELETE proposals still held for a reviewer). Remove
# approver_role to put every other write behind `vouch review`.
"approver_role": "trusted-agent",
# phase d — the receipt is the reviewer. When true, a claim whose
# byte-offset receipts all verify is auto-approved with no human;
# a claim that cannot quote its source is left pending, as is
# every page/entity/relation proposal. Set false to put every
# write behind `vouch review`.
# a claim that cannot quote its source is left pending. Only
# consulted when approver_role does not already clear the write.
"auto_approve_on_receipt": True,
# opt in by setting `approver_role: trusted-agent` to let the
# capturing agent's proposals self-approve through the same
# proposals.approve() gate. Left unset so writes stay behind
# `vouch review` unless the receipt gate below clears them.
# phase d — the receipt is the reviewer. When true, a claim whose
# byte-offset receipts all verify is auto-approved with no human;
# a claim that cannot quote its source is left pending. Only
# consulted when approver_role does not already clear the write.
"auto_approve_on_receipt": True,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/storage.py` around lines 84 - 94, Remove the default
"approver_role": "trusted-agent" entry from the starter configuration in the
initialization flow so the review gate remains enabled by default. Keep
auto_approve_on_receipt enabled as the safe default, and update vouch init to
surface the trusted-agent shortcut as an explicit opt-in configuration rather
than enabling it implicitly.

Sources: Coding guidelines, Path instructions

Comment on lines +38 to +41
def _rpc(method: str, params: dict) -> dict:
resp = handle_request({"id": "t", "method": method, "params": params})
assert resp["ok"], resp
return resp["result"] # type: ignore[no-any-return]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Envelope-shape assertions incomplete for new kb.* method tests.

Per coding guidelines, new kb.* method tests must assert the full JSONL envelope shape ({id, ok, result} on success, {id, ok: false, error} on failure). Here only ok is checked; id echo and error presence on failure are never asserted.

As per coding guidelines, "For each new kb.* method, add a test that asserts the JSONL envelope shape: {id, ok, result} on success and {id, ok: false, error} on failure."

✅ Proposed fix
 def _rpc(method: str, params: dict) -> dict:
     resp = handle_request({"id": "t", "method": method, "params": params})
     assert resp["ok"], resp
+    assert resp["id"] == "t"
     return resp["result"]  # type: ignore[no-any-return]
 def test_jsonl_read_missing_is_an_error(
     store: KBStore, monkeypatch: pytest.MonkeyPatch, method: str, params: dict
 ) -> None:
     monkeypatch.chdir(store.root)
     resp = handle_request({"id": "t", "method": method, "params": params})
     assert not resp["ok"]
+    assert "error" in resp

Also applies to: 69-74

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_read_evidence_source.py` around lines 38 - 41, Update the _rpc
helper and the related kb.* method tests to validate the complete JSONL response
envelope: assert the echoed id and successful result, and add failure-path
assertions for ok=false with an error field. Apply the same envelope checks to
the tests referenced around lines 69-74 while preserving their existing
method-specific assertions.

Source: Coding guidelines

Comment on lines +27 to +28
REPO = Path(__file__).resolve().parents[1]
RELEVANCE_GUARD = REPO / "contrib" / "strategies" / "relevance_guard.py"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

test against the actual reigning champion.

relevance_guard.py is static, while promotions update contrib/strategies/baseline.py. Load the baseline file here so this invariant catches drift after a champion promotion.

Also applies to: 55-61

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_strategy_configured.py` around lines 27 - 28, Update the
configured-strategy tests to load the current champion from
contrib/strategies/baseline.py instead of the static RELEVANCE_GUARD path.
Adjust the affected test setup and assertions around the referenced lines so the
invariant compares against the baseline file that promotions update, while
preserving the existing test behavior.

Comment on lines +38 to +44
def test_starter_config_defaults_to_trusted_agent(store: KBStore) -> None:
# a fresh kb ships with auto approval on: no config edit, no human step.
import yaml

loaded = yaml.safe_load(store.config_path.read_text(encoding="utf-8"))
assert loaded["review"]["approver_role"] == "trusted-agent"
assert loaded["review"]["auto_approve_on_receipt"] is True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

keep trusted-agent approval opt-in.

This test codifies a starter default that drains pending writes without a human review step. Default to a non-trusted approver and require an explicit review.approver_role: trusted-agent opt-in instead.

As per coding guidelines, “do not ... remove the review gate for trusted agents”; as per path instructions, the trusted-agent shortcut is allowed only as an opt-in.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_trusted_agent_auto_approve.py` around lines 38 - 44, Update the
starter configuration defaults exercised by
test_starter_config_defaults_to_trusted_agent so review.approver_role is a
non-trusted approver and auto_approve_on_receipt does not bypass human review by
default. Preserve trusted-agent approval only when review.approver_role is
explicitly set to trusted-agent, and revise the test assertions to verify the
opt-in requirement.

Sources: Coding guidelines, Path instructions

Comment on lines +235 to +252
// Kindless ids (relation endpoints, audit object ids): probe the readable
// kinds and open the first that resolves. Proposal ids resolve to nothing.
const resolveOpen =
onOpen &&
(async (id: string) => {
for (const kind of RESOLVE_ORDER) {
if (!canRead(project, kind)) continue
try {
await rpc(project.conn, READ_METHOD[kind].method, { [READ_METHOD[kind].param]: id })
onOpen(kind, id)
return
} catch {
// not this kind — try the next
}
}
toast('error', `${id.slice(0, 24)} is not a readable artifact (a proposal, or deleted)`)
})
const resolve = resolveOpen ? (id: string) => void resolveOpen(id) : undefined

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg '(^|/)ArtifactDrawer\.tsx$|(^|/)ChatView\.tsx$|(^|/)rpc\.ts$|(^|/)rpc\.(tsx?)$' || true

echo "== ArtifactDrawer relevant section =="
fs="webapp/src/components/ArtifactDrawer.tsx"
if [ -f "$fs" ]; then
  sed -n '200,280p' "$fs" | nl -ba -v200
fi

echo "== ChatView around 333 =="
cfs="webapp/src/views/ChatView.tsx"
if [ -f "$cfs" ]; then
  sed -n '310,355p' "$cfs" | nl -ba -v310
fi

echo "== rpc exports/usages =="
for f in $(git ls-files | rg '(^|/)webapp/src[^/]*(rpc|api|lib)[^/]*\.(ts|tsx)$'); do
  if rg -q 'export .*VouchHttpError|class .*VouchHttpError|function .*VouchHttpError|interface .*RpcError|throw .*Error|type RpcError' "$f"; then
    echo "--- $f ---"
    rg -n 'VouchHttpError|RpcError|rpc\(|throw .*Error|interface .*|export .*Error' "$f" | head -80
  fi
done

Repository: vouchdev/vouch

Length of output: 343


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ArtifactDrawer relevant section =="
fs="webapp/src/components/ArtifactDrawer.tsx"
sed -n '1,320p' "$fs" | awk '{printf "%5d\t%s\n", NR, $0}' | sed -n '220,270p'

echo "== ChatView around 333 =="
cfs="webapp/src/views/ChatView.tsx"
sed -n '300,365p' "$cfs" | awk '{printf "%5d\t%s\n", NR, $0}'

echo "== rpc.ts contents =="
rfs="webapp/src/lib/rpc.ts"
sed -n '1,240p' "$rfs" | awk '{printf "%5d\t%s\n", NR, $0}'

echo "== usages of throw/catch around rpc/rpc function in ArtifactDrawer and ChatView =="
rg -n "catch\s*\(|try\s*\{|rpc\(|VouchHttpError|VouchError|throw" webapp/src/components/ArtifactDrawer.tsx webapp/src/views/ChatView.tsx webapp/src/lib/rpc.ts

Repository: vouchdev/vouch

Length of output: 12209


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== ArtifactDrawer imports and READ_METHOD/RESOLVE_ORDER =="
sed -n '1,80p' webapp/src/components/ArtifactDrawer.tsx | awk '{printf "%5d\t%s\n", NR, $0}'
rg -n "READ_METHOD|RESOLVE_ORDER|canRead|useErrorToast|import .*rpc|import .*Vouch" webapp/src/components/ArtifactDrawer.tsx

echo "== ArtifactDrawer full toast/error handling section =="
sed -n '212,255p' webapp/src/components/ArtifactDrawer.tsx | awk '{printf "%5d\t%s\n", NR, $0}'

echo "== RPC request/error probe (source-only behavioral extractor) =="
python3 - <<'PY'
from pathlib import Path
p = Path("webapp/src/lib/rpc.ts").read_text()
def has(pattern):
    print(f"{pattern!r} present: {pattern in p}")
has("throw new VouchHttpError")
has("throw new VouchRpcError")
print("fetch catch absent:", "\n" in p and "fetch('/proxy/rpc" in p and "catch" not in p.split("fetch('/proxy/rpc", 1)[-1].split("}", 1)[0][:500])
PY

Repository: vouchdev/vouch

Length of output: 6635


Only continue on RPC missing-artifact errors

rpc() exposes VouchHttpError for auth/transport and VouchRpcError for server errors, but the bare catch treats 401, non-2xx HTTP status, and dropped connections as “wrong kind” and then shows “not a readable artifact”. Re-throw VouchHttpError and continue only for RPC errors that mean this id isn’t that artifact.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/ArtifactDrawer.tsx` around lines 235 - 252, Update the
catch block in resolveOpen to rethrow VouchHttpError for authentication,
transport, and non-2xx failures, and continue probing only when the caught
VouchRpcError represents a missing artifact for the current kind. Preserve the
existing success callback and final toast behavior for ids that do not resolve
in any readable kind.

citations went dead for a specific slice of ids and never worked at all
inside page bodies.

`_slugify` strips dashes before truncating to 60 chars, so cutting mid-word
re-introduces a trailing dash — 135 of 1585 claim ids and 7 of 104 page ids
here end in `-`. the client matcher required the last character to be
alphanumeric, so all of those rendered as dead text. separately the
`[claim: <id>]` marker, which is vouch's canonical inline reference in page
bodies (compile.py `_CLAIM_MARKER_RE`), never matched at all.

the matcher now accepts trailing-dash ids and an optional `kind:` prefix,
while still leaving markdown task boxes, footnote refs and `[[wikilinks]]`
as prose. a remark plugin rewrites bracketed ids in markdown into `vouch:`
links, so page bodies, session summaries, claude answers and proposal
payloads all resolve; a custom urlTransform is needed because react-markdown
otherwise sanitises that scheme away. code spans stay literal, and evidence
quotes are deliberately left verbatim — they are receipts, and styling text
inside them as a link misrepresents the span.

the drawer's kind probe moves to lib/resolveArtifact so chat, claims and the
review queue share it, widened from three kinds to six with a shape hint so
`ev-…` and 64-char source hashes cost one read instead of six. views without
a drawer of their own route through /browse/<kind>/<id>. a named kind opens
directly rather than being caps-gated: synthesize already verified it, and a
failed read reports itself better than a guess.

also rstrip the slug after truncating so new ids stop growing the defect.
existing ids are untouched; the matcher fix is what makes those 142 work.

verified against this kb: 43 `[claim: id]` markers across 5 pages now
linkify, 0 of 5859 real ids fail to match, wikilinks intact.
the propose-time payload validation added a page->claim existence check
to _payload_block_reason. approve() runs that precheck before its own
dead-claim-refs handling, so a page citing a deleted claim raised a flat
ProposalError and DeadClaimRefsError could never surface -- the
drop_missing_claims strip path became unreachable and three
tests/test_dead_claim_refs.py cases went red on test.

approve() now passes skip_dead_claim_refs=True; check_approvable keeps
the check so the batch cli precheck still flags dangling refs and stays
all-or-nothing. the storage test that asserted the old wording now pins
the specific DeadClaimRefsError and that the page body is untouched.
picks up the approve-gate fix (#601) so this branch's ci is green on its
own base rather than inheriting the three red dead-claim-refs cases from
test.
…adow

fix(proposals): stop payload precheck shadowing dead claim refs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/vouch/cli.py (1)

5569-5572: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

turn invalid --seeds into usage errors.

--seeds "abc" or "1,x" raises an unhandled ValueError, and values like " ,," parse to an empty seed list. Validate each token before adding it to seed_list and reject an empty list with a click parameter error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/cli.py` around lines 5569 - 5572, Update the seed parsing that
builds seed_list to validate every --seeds token as an integer, converting
invalid values and an empty resulting list into a Click parameter error rather
than allowing ValueError or empty input to proceed. Preserve the existing
single-seed fallback when --seeds is omitted.
🧹 Nitpick comments (2)
src/vouch/proposals.py (1)

877-886: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Dropping every cited claim can flip the page into a PageKindError refusal.

validate_page(..., has_citations=bool(page.claims or page.sources)) runs right after the strip; if all claims were missing and the page has no sources, an explicit drop_missing_claims=True retry fails with a citation-requirement error instead of a message tied to the drop. Worth surfacing that as a dedicated reason so the reviewer knows stripping is not a viable remediation for that proposal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/proposals.py` around lines 877 - 886, Update the missing-claim
handling around missing_claim_refs and the drop_missing_claims branch to detect
when removing missing claims leaves the proposal with neither claims nor
sources. Surface a dedicated drop-related refusal reason before or alongside
validate_page, while preserving normal stripping behavior when citations remain.
src/vouch/contradictions.py (1)

107-141: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Full-KB load plus per-entity pairwise scoring — fine for a CLI, worth bounding later.

find_candidates() loads every claim and every relation/proposal (via _already_flagged_pairs) and then compares all same-entity pairs; a hub-entity with many claims makes this quadratic on tokenized text. An entity-group size cap or precomputed token sets per claim would keep the advisory scan predictable on large KBs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/contradictions.py` around lines 107 - 141, The candidate scan in
find_candidates can become unbounded for large entity groups. Add a predictable
bound, such as a maximum group size, before the nested pairwise scoring loop, or
reuse precomputed token sets to reduce repeated text work; preserve read-only
behavior and existing filtering, threshold, deduplication, and sorting
semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/trust-gate.yml:
- Around line 29-30: The GitHub REST file listing can truncate at 3,000 files,
so each workflow must fail closed before classifying changes. In
.github/workflows/trust-gate.yml lines 29-30, fetch changed_files metadata and
abort when it exceeds 3,000 before invoking vouch.pr_bot changed-files; apply
the same pre-classification check in .github/workflows/auto-merge.yml lines
73-74 and before adding auto-merge in .github/workflows/comment-command.yml
lines 55-56.

In `@src/vouch/cli.py`:
- Around line 3731-3732: Update the contradict-scan CLI’s --limit option to use
click.IntRange(min=0), ensuring negative values are rejected before the command
invokes contradictions.scan(). Keep the existing None default and valid
non-negative limit behavior unchanged.

In `@src/vouch/contradictions.py`:
- Around line 81-104: The _already_flagged_pairs function must also exclude
contradiction pairs from decided proposals, including rejected proposals, so
prior reviewer decisions are not re-proposed. Extend its proposal scan beyond
ProposalStatus.PENDING to include decided statuses, while preserving the
existing relation-kind and source/target payload checks.

In `@webapp/src/components/Markdown.tsx`:
- Line 26: Validate custom vouch URLs before preserving them: in
webapp/src/components/Markdown.tsx#L26, call parseVouchHref(url) and retain the
custom scheme only when parsing succeeds. In webapp/src/lib/vouchIds.ts#L16-L24,
make parseVouchHref safely handle malformed decodeURIComponent input and reject
prefixes not included in ArtifactKind; add tests covering malformed encoding and
unknown kinds.

---

Outside diff comments:
In `@src/vouch/cli.py`:
- Around line 5569-5572: Update the seed parsing that builds seed_list to
validate every --seeds token as an integer, converting invalid values and an
empty resulting list into a Click parameter error rather than allowing
ValueError or empty input to proceed. Preserve the existing single-seed fallback
when --seeds is omitted.

---

Nitpick comments:
In `@src/vouch/contradictions.py`:
- Around line 107-141: The candidate scan in find_candidates can become
unbounded for large entity groups. Add a predictable bound, such as a maximum
group size, before the nested pairwise scoring loop, or reuse precomputed token
sets to reduce repeated text work; preserve read-only behavior and existing
filtering, threshold, deduplication, and sorting semantics.

In `@src/vouch/proposals.py`:
- Around line 877-886: Update the missing-claim handling around
missing_claim_refs and the drop_missing_claims branch to detect when removing
missing claims leaves the proposal with neither claims nor sources. Surface a
dedicated drop-related refusal reason before or alongside validate_page, while
preserving normal stripping behavior when citations remain.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 603282ad-2915-4a9f-b0cd-8f1a132a06fa

📥 Commits

Reviewing files that changed from the base of the PR and between 12be9c3 and 0ef2f87.

📒 Files selected for processing (36)
  • .github/workflows/auto-merge.yml
  • .github/workflows/coderabbit-gate.yml
  • .github/workflows/comment-command.yml
  • .github/workflows/trust-gate.yml
  • CHANGELOG.md
  • src/vouch/cli.py
  • src/vouch/contradictions.py
  • src/vouch/dual_solve.py
  • src/vouch/pr_bot.py
  • src/vouch/proposals.py
  • src/vouch/recall.py
  • src/vouch/storage.py
  • src/vouch/transcript.py
  • tests/test_capture.py
  • tests/test_contradictions.py
  • tests/test_delete.py
  • tests/test_dual_solve.py
  • tests/test_pr_bot.py
  • tests/test_recall.py
  • tests/test_session_transcript.py
  • tests/test_storage.py
  • webapp/src/components/ArtifactDrawer.tsx
  • webapp/src/components/CitedText.tsx
  • webapp/src/components/IdChip.tsx
  • webapp/src/components/Markdown.test.tsx
  • webapp/src/components/Markdown.tsx
  • webapp/src/lib/citations.test.ts
  • webapp/src/lib/citations.ts
  • webapp/src/lib/resolveArtifact.ts
  • webapp/src/lib/useOpenArtifact.ts
  • webapp/src/lib/vouchIds.ts
  • webapp/src/views/ChatView.tsx
  • webapp/src/views/ClaimsView.test.tsx
  • webapp/src/views/ClaimsView.tsx
  • webapp/src/views/PendingView.test.tsx
  • webapp/src/views/PendingView.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • webapp/src/views/PendingView.test.tsx
  • tests/test_recall.py
  • CHANGELOG.md

Comment on lines +29 to +30
gh api "repos/$REPO/pulls/$PR/files" --paginate > files.json
PYTHONPATH=src python -m vouch.pr_bot changed-files --json-file files.json > changed.txt

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

fail closed when the REST file listing is truncated.

GitHub’s REST PR-files endpoint returns at most 3,000 files even with pagination, so a protected path beyond that ceiling is omitted from classification. This can bypass the core-path trust gate and incorrectly arm auto-merge. (docs.github.com)

  • .github/workflows/trust-gate.yml#L29-L30: fetch changed_files metadata and fail if it exceeds 3,000 before classifying.
  • .github/workflows/auto-merge.yml#L73-L74: apply the same fail-closed check before deriving the merge class.
  • .github/workflows/comment-command.yml#L55-L56: apply the same fail-closed check before adding auto-merge.

As per path instructions, these are “core, security-sensitive workflows.”

📍 Affects 3 files
  • .github/workflows/trust-gate.yml#L29-L30 (this comment)
  • .github/workflows/auto-merge.yml#L73-L74
  • .github/workflows/comment-command.yml#L55-L56
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/trust-gate.yml around lines 29 - 30, The GitHub REST file
listing can truncate at 3,000 files, so each workflow must fail closed before
classifying changes. In .github/workflows/trust-gate.yml lines 29-30, fetch
changed_files metadata and abort when it exceeds 3,000 before invoking
vouch.pr_bot changed-files; apply the same pre-classification check in
.github/workflows/auto-merge.yml lines 73-74 and before adding auto-merge in
.github/workflows/comment-command.yml lines 55-56.

Source: Path instructions

Comment thread src/vouch/cli.py
Comment on lines +3731 to +3732
@click.option("--limit", default=None, type=int,
help="Cap the number of pairs proposed in one run.")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Locate cli.py and contradictions.scan definitions"
fd -a 'cli.py|capacities.py|capabilities.py' . | sed 's#^\./##' | head -50
echo

rg -n "def scan|class .*Contradictions|def contradict_scan|`@click.option`.*--limit|scan\\(" src/vouch tests -S | sed -n '1,200p'

echo
echo "Relevant cli.py sections"
file=$(fd 'cli.py' src/vouch | head -1)
if [ -n "$file" ]; then
  wc -l "$file"
  sed -n '3680,3770p' "$file" | cat -n
fi

echo
echo "Relevant scan implementations"
rg -n -A 40 -B 10 "def scan|candidates\\[:-limit\\]|limit|scan\\(" src/vouch -S | sed -n '1,260p'

Repository: vouchdev/vouch

Length of output: 22734


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "src/vouch/contradictions.py outline and limit handling"
ast-grep outline src/vouch/contradictions.py --view expanded | sed -n '1,160p'
echo
sed -n '130,230p' src/vouch/contradictions.py | cat -n

echo
echo "MCP and JSONL references to contradict scan/limit"
rg -n -A 10 -B 8 "contradict|kb_.*contrad|scan\\(" src/vouch/server.py src/vouch/jsonl_server.py src/vouch/capabilities.py | sed -n '1,240p'

echo
echo "Behavioral probe for Python slice semantics"
python3 - <<'PY'
for n in range(3):
    candidates = list(range(8 + n))
    for m in [-1, -2, 3, 0, None]:
        result_count = len(candidates[None if m is None else :m])
        print({
            "n": n,
            "m": m,
            "candidates": candidates,
            "result": candidates[None if m is None else :m],
            "result_count": result_count,
        })
PY

Repository: vouchdev/vouch

Length of output: 12662


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Python slice semantics probe"
python3 - <<'PY'
for n in range(3):
    candidates = list(range(8 + n))
    for m in [-1, -2, 3, 0, None]:
        result = candidates[:m]
        print(dict(
            n=n,
            m=m,
            candidates=list(candidates),
            result=result,
            result_count=len(result),
            original_count=len(candidates),
            count_dropped=len(candidates) - len(result),
        ))
PY

echo
echo "Check available surfaces for contradict scan"
python3 - <<'PY'
from pathlib import Path
roots = [Path(p) for p in ("src/vouch/server.py","src/vouch/jsonl_server.py","src/vouch/capabilities.py","src/vouch/cli.py")]
for root in roots:
    text = root.read_text()
    print(root, "has contradict/dedup scan:", "contradict" in text and "scan" in text)
    for needle in ["contradict_scan", "kb_contradict_scan", "kb_dedup_scan", "dedup_scan"]:
        if needle in text:
            print("  ", needle)
PY

Repository: vouchdev/vouch

Length of output: 2773


validate --limit in the CLI path.

contradict-scan passes limit=None through to contradictions.scan(), but only the CLI option is being changed here. Use click.IntRange(min=0) for the CLI option and validate/reject negative limits before scan() applies it as a Python slice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/cli.py` around lines 3731 - 3732, Update the contradict-scan CLI’s
--limit option to use click.IntRange(min=0), ensuring negative values are
rejected before the command invokes contradictions.scan(). Keep the existing
None default and valid non-negative limit behavior unchanged.

Comment on lines +81 to +104
def _already_flagged_pairs(store: KBStore) -> set[frozenset[str]]:
"""Pairs that already have a contradiction on record, in any form.

Covers claims cross-linked via `Claim.contradicts` (set by
`lifecycle.contradict`), claims already joined by an approved
`RelationType.CONTRADICTS` edge (e.g. a prior scan's proposal that was
approved), and pairs with a still-pending `contradicts` relation
proposal from an earlier scan run. Any one of these means re-proposing
the pair would be a duplicate.
"""
flagged: set[frozenset[str]] = set()
for c in store.list_claims():
for other in c.contradicts:
flagged.add(frozenset({c.id, other}))
for rel in store.list_relations():
if rel.relation == RelationType.CONTRADICTS:
flagged.add(frozenset({rel.source, rel.target}))
for p in store.list_proposals(ProposalStatus.PENDING):
if p.kind == ProposalKind.RELATION and p.payload.get("relation") == "contradicts":
src = p.payload.get("source")
target = p.payload.get("target")
if src and target:
flagged.add(frozenset({src, target}))
return flagged

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Rejected pairs from earlier scans get re-proposed.

_already_flagged_pairs() covers Claim.contradicts, approved CONTRADICTS edges, and pending proposals — but not decided/rejected ones. A reviewer who rejects a false positive will see the same pair re-filed on the next scan(dry_run=False), which erodes trust in the queue. Consider also scanning decided proposals (or recording rejections) for contradicts pairs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/vouch/contradictions.py` around lines 81 - 104, The
_already_flagged_pairs function must also exclude contradiction pairs from
decided proposals, including rejected proposals, so prior reviewer decisions are
not re-proposed. Extend its proposal scan beyond ProposalStatus.PENDING to
include decided statuses, while preserving the existing relation-kind and
source/target payload checks.

// `vouch:` hrefs are minted by remarkVouchIds, never by the author's
// markdown, and are consumed by the anchor override below rather than
// reaching the DOM. Everything else keeps the default sanitiser.
urlTransform={(url) => (url.startsWith(VOUCH_SCHEME) ? url : defaultUrlTransform(url))}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate custom vouch: hrefs before preserving them.

An author can supply [label](vouch:claim/%E0%A4%A): Line 26 bypasses the normal URL sanitizer, then decodeURIComponent throws while rendering. Unknown kinds are also cast as valid artifact kinds. Safely decode and whitelist kinds in parseVouchHref, then preserve vouch: URLs only when parsing succeeds.

  • webapp/src/components/Markdown.tsx#L26-L26: call parseVouchHref(url) before retaining a custom-scheme URL.
  • webapp/src/lib/vouchIds.ts#L16-L24: catch malformed URI decoding and reject non-ArtifactKind prefixes; add malformed and unknown-kind tests.
📍 Affects 2 files
  • webapp/src/components/Markdown.tsx#L26-L26 (this comment)
  • webapp/src/lib/vouchIds.ts#L16-L24
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@webapp/src/components/Markdown.tsx` at line 26, Validate custom vouch URLs
before preserving them: in webapp/src/components/Markdown.tsx#L26, call
parseVouchHref(url) and retain the custom scheme only when parsing succeeds. In
webapp/src/lib/vouchIds.ts#L16-L24, make parseVouchHref safely handle malformed
decodeURIComponent input and reject prefixes not included in ArtifactKind; add
tests covering malformed encoding and unknown kinds.

…opose-time

fix(proposals): validate claim/relation/entity payloads at propose time
…e-status

fix(vault-sync): mirror post-approve WORKING/DRAFT artifacts (#583)
fix(secrets): mask JSON/quoted-key credentials
…thout-kb

fix(transcript): degrade session_transcript when no kb resolves
fix(context): apply rerank on embedding and fts5 backends too
…d-pages

fix(context): drop archived pages from context packs
…d-citation-547

fix(vault-sync): surface deleted-citation as VaultSyncError
@plind-junior
plind-junior merged commit dc937eb into main Jul 30, 2026
21 of 23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

adapters agent host adapters and install manifests ci: passing ci is green ci github actions and automation cli command line interface docs documentation, specs, examples, and repo guidance dual-solve dual-solve orchestration mcp mcp, jsonl, and http surfaces retrieval context, search, synthesis, and evaluation review-ui browser review ui schemas json schemas and generated schema assets size: XL 1000 or more changed non-doc lines storage kb storage, migrations, schemas, and proposals sync sync, vault mirror, and diff flows tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.