Test#420
Conversation
fix: track backend label separately in JSONL search handler (#14)
fix: add existence guards to all put_* methods (#12)
…act-guard fix(proposals): refuse to overwrite existing artifact on approve
kb.register_source_from_path read the contents of any file the process could access, letting an agent register /etc/passwd, ~/.ssh/id_rsa, ~/.aws/credentials etc. as a "source" and then retrieve the bytes via kb.cite or kb.list_sources. Resolve the path (following symlinks) and require it to be inside the KB root before reading. Adds KBStore.resolve_under_root() so both the JSONL handler and the MCP tool share one containment check. Fixes #10
The CLI handlers for approve and reject caught only (ArtifactNotFoundError, ValueError) but proposals.approve() / proposals.reject() raise ProposalError -- a RuntimeError subclass. The four propose-* shortcuts caught nothing at all. Result: a double- approve, an empty rejection reason, an empty claim text, or an unknown source id surfaced a raw Python traceback instead of the one-line `Error: ...` the rest of the CLI emits. Add a small `_cli_errors` context manager that translates ArtifactNotFoundError / ValueError / ProposalError / LifecycleError into click.ClickException, and apply it to every command that calls into proposals, lifecycle, sessions, or storage. The MCP and JSONL servers already do the equivalent in their own envelopes; this brings the human-facing surface in line. Adds tests/test_cli.py with regressions for approve, reject, propose- claim, propose-entity, and show.
ruff B904 requires `raise ... from err` inside an except block so the original exception is preserved on the chained __cause__. Without it, the CI lint step rejects the file. The seven sites are pre-existing (put_claim, put_page, put_entity, put_relation, put_evidence, put_session, put_proposal); this PR inherited the failure from main rather than introducing it, but the path traversal fix cannot land until lint is green, so the cleanup happens here. No behavior change: the chained exception carries .__cause__ but the public ValueError message and type are unchanged.
verify_source() caught FileNotFoundError, but store.read_source_content() raises ArtifactNotFoundError (a KeyError subclass) when the content blob is missing -- so the "stored content missing" graceful path never ran and a single broken source crashed the entire verify_all() sweep, breaking `vouch source verify` and `vouch doctor`. The same call can also raise OSError (permission denied, TOCTOU race between exists() and read_bytes(), underlying I/O error) which was likewise unhandled. Catch both: the existing "missing" path keeps its note, and OSError surfaces as "stored content unreadable: <reason>". Adds three regression tests: - missing-content blob -> graceful per-source failure - unreadable stored content (monkeypatched PermissionError) -> graceful per-source failure with the underlying reason in the note - mixed sweep with one good + one broken source -> verify_all returns both results instead of aborting at the first failure Fixes #30
The CI workflow runs `ruff check`, `mypy`, then `pytest`, and the fix/cli-clean-domain-errors branch was failing all three on pre-existing main-branch issues unrelated to the CLI change itself. * ruff: add `from e` to the seven `raise ValueError(...) from FileExistsError` re-raises added by the recent exclusive-create guards in storage.put_* (B904), and let ruff re-sort the cli.py import block (I001). * mypy: narrow the `kind` value flowing into `ContextItem(type=...)` with a `Literal` cast so the strict-typed field accepts what the search backends actually return. * pytest: `session_end()` mutates a session that `session_start()` has already written, but `put_session()` now uses exclusive create and rejects the second write. Add `KBStore.update_session()` mirroring `update_claim` and have `session_end` call it; existing test_sessions coverage now passes again. No behavior change for the CLI surface — these are infrastructure fixes so the existing test_sessions / lint / type assertions pass on this branch.
The B904 lint failures were already addressed in 2a439a5, but the CI matrix still fails on two pre-existing main-branch issues: * mypy: context.py:64 passed `kind: str` into ContextItem.type, which is Literal["claim","page","entity","relation","source"]. Narrow it with a typing.cast so strict mode accepts the search-backend output. * pytest: session_end() mutates a session that session_start() already wrote, but put_session() switched to exclusive create and rejects the second write. Add KBStore.update_session() mirroring update_claim and have session_end call it; test_sessions coverage passes again. No behavior change for the path-traversal fix itself.
CodeRabbit on PR #28 noted that resolve_under_root() only validated a pathname snapshot. Callers then re-opened the same name with .is_file() and .read_bytes(), so an attacker who can swap the resolved path for a symlink between the containment check and the read can still exfiltrate an out-of-root file via kb.register_source_from_path. Collapse the validate-then-read into a single trusted helper `KBStore.read_under_root(path)` that returns `(resolved, bytes)`: 1. Path.resolve() chases pre-existing symlinks and the resulting target is checked for containment (existing behaviour -- legitimate in-root symlinks still work). 2. The read goes through `os.open(resolved, O_RDONLY | O_NOFOLLOW)` so a fresh symlink placed at the resolved name *after* the check fails with ELOOP rather than following the swap. 3. `fstat` + `S_ISREG` rejects directories / device nodes / pipes atomically on the same fd, replacing the racy `is_file()` test the callers used to do. Both register_source_from_path handlers (MCP + JSONL) switch to the new helper and drop their now-redundant follow-up checks. Adds tests: - symlink swapped into the resolved name -> rejected via ELOOP - directory at a valid path -> rejected via S_ISREG Existing "outside the root" and "inside the root" tests still pass.
…versal fix(server): block path traversal in register_source_from_path
ruff B904 requires `raise ... from err` inside an except block so the original exception is preserved on the chained __cause__. Without it, the CI lint step rejects the file. The seven sites are pre-existing (put_claim, put_page, put_entity, put_relation, put_evidence, put_session, put_proposal); this PR inherited the failure from main rather than introducing it, but the path traversal fix cannot land until lint is green, so the cleanup happens here. No behavior change: the chained exception carries .__cause__ but the public ValueError message and type are unchanged.
The B904 lint failures were already addressed in 2a439a5, but the CI matrix still fails on two pre-existing main-branch issues: * mypy: context.py:64 passed `kind: str` into ContextItem.type, which is Literal["claim","page","entity","relation","source"]. Narrow it with a typing.cast so strict mode accepts the search-backend output. * pytest: session_end() mutates a session that session_start() already wrote, but put_session() switched to exclusive create and rejects the second write. Add KBStore.update_session() mirroring update_claim and have session_end call it; test_sessions coverage passes again. No behavior change for the path-traversal fix itself.
Resolve conflicts in context.py, sessions.py, storage.py (keep main). fix: block bad writes in import_apply per review feedback (#13) import_apply now captures schema validation issues and skips the file instead of passing a throwaway list.
fix(cli): translate domain errors into clean ClickException output
fix(bundle): reject path traversal in import (CVE-2007-4559, #9)
fix(verify): catch ArtifactNotFoundError on missing stored content
Approved design for feat/semantic-search. Embedding (sentence- transformers all-mpnet-base-v2) becomes the primary search backend with FTS5 as fallback. Synchronous-at-write indexing across all six artifact types (claim, page, source, entity, relation, evidence). Maximally functional scope (~3000 LOC): pluggable model adapter registry, sqlite-vec ANN with NumPy fallback, cross-encoder rerank, HyDE query expansion, ingest-time duplicate detection, model-identity migration, recall/MRR/nDCG eval harness, and full CLI/MCP/JSONL parity. The writing-plans step will turn the rollout order in section 16 into concrete phased tasks.
32-task TDD plan for the semantic-search feature: foundation, storage, write hooks across all six artifact types, semantic-primary search integration in MCP/JSONL/CLI, RRF fusion + hybrid, cross-encoder rerank, HyDE expansion, ingest-time duplicate detection, model-identity migration, recall/MRR/nDCG scorer, end-to-end integration test, and user docs. Each task: failing test, minimal implementation, passing test, commit. MockEmbedder test double keeps the unit suite fast; the real model is exercised only under @pytest.mark.integration. The evaluation module is named scorer.py rather than eval.py to avoid shadowing the Python builtin and to keep static analysers quiet; the user-facing CLI subcommand remains `vouch eval embedding` (the Click group is registered under the name "eval", with the Python identifier eval_group).
CI ran ruff which flagged SIM105 on the three try/except ImportError guard blocks in src/vouch/embeddings/__init__.py. Replace each with `contextlib.suppress(ImportError)` -- same semantics, satisfies ruff. Also add mypy overrides for numpy / sqlite_vec / sentence_transformers / fastembed so the type check passes in the base [dev] CI install where those optional extras aren't present. The embedding code paths that import these libraries are only reached when the extras are installed at runtime; for the static type check the missing stubs are noise.
CI runs `pip install -e '.[dev]'` which deliberately excludes the
optional `[embeddings]` extras. tests/embeddings/test_*.py modules
import numpy at top level, so pytest collection fails with
ModuleNotFoundError before any test can be deselected.
Add tests/embeddings/conftest.py with `pytest.importorskip("numpy")`
so the entire embeddings test directory skips gracefully when numpy
is absent. Once `pip install vouch[embeddings]` (or numpy itself) is
present, the skip is a no-op and the tests run normally.
`_validate_content` in bundle.py uses the path's first directory component to look up a Pydantic validator. For sources, both `sources/<sha>/meta.yaml` (the Source model) and `sources/<sha>/content` (raw opaque bytes) hit the same "sources" key, so the validator was being run on the raw content bytes and failing with "1 validation error for Source". Add an early return for any non-meta.yaml path under `sources/` so opaque content bytes are not Pydantic-validated. Only the Source metadata file is checked, which matches the original intent of the PR #13 validation. Unblocks three pre-existing test_bundle.py failures inherited from upstream main.
Add back KB_FORMAT_VERSION, SCHEMA_VERSION, SCHEMA_VERSION_FILENAME, _starter_config to storage.py and _RETRACTED_CLAIM_STATUSES to context.py to fix import errors in test suite.
the test-branch merges (2ddd1f9 onward) took main's older, smaller storage.py / context.py / index_db.py / cli.py / server.py / health.py / jsonl_server.py / bundle.py / sessions.py while keeping the newer caller modules (lifecycle, proposals, graph, notify, sync, provenance, codex_rollout, triage). that desync left 31 mypy errors — callers referencing methods the truncated backing no longer defined (put_relation_idempotent, _validate_claim_refs, update_page, update_proposal, index_prov_edge, get_meta) — so the test job failed at mypy before pytest ever ran. restore src/ and tests/ to bb06565, the last coherent snapshot, where the fuller backing matches the callers. this also brings back the reindex cli command the recall-eval job invokes (the truncated cli only had index), fixing the second red job. keep __version__ at 1.2.2 so the four version sites stay in lockstep.
contributors should attach screenshots from the vouch-ui webapp as proof when their PR changes the CLI output or user-facing behavior.
contributors should attach screenshots from the vouch-ui webapp as proof when their PR changes the CLI output or user-facing behavior.
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 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/security-audit.yml:
- Line 27: The security-audit workflow is using actions/checkout with default
persisted credentials, which leaves the token stored in local git config
unnecessarily. Update the checkout step in the workflow to disable credential
persistence by setting persist-credentials to false on the actions/checkout@v4
step so the PR-controlled code cannot reuse the token.
In @.vouch/captures/1218169c-de25-48f4-bd93-70ff77d2fda2.jsonl:
- Around line 68-71: Remove the auth/account probing from this capture by
redacting the `gh` account checks and any direct references to
`/home/a/.config/mcp-publisher/token.json`, since they expose machine-specific
identity and token location details. Keep the surrounding publish/debug context
in the JSONL entry, but replace those sensitive commands and paths with generic
redactions so the `mcp-publisher` capture can be safely shared. Use the existing
capture entries in this JSONL file as the place to scrub the sensitive lines
before commit.
In @.vouch/decided/20260707-092531-154cb163.yaml:
- Around line 42-45: The rejection record uses a placeholder decision_reason
value, which leaves the audit trail unclear. Update the rejected entry in the
YAML so the decided_at/decided_by metadata remains intact but decision_reason is
replaced with a short human-readable explanation, or remove decision_reason
entirely if no reason is available yet.
In
@.vouch/pages/session-what-are-the-non-development-related-files-and-folde.md:
- Around line 36-38: The session snapshot includes absolute local paths that
leak the developer’s home and temp directory layout. Redact these entries in the
session markdown and the matching `.vouch/decided/...yaml` artifact by replacing
them with repo-relative paths or neutral placeholders, using the same path list
entries shown in the snapshot.
In `@src/vouch/health.py`:
- Around line 568-626: The progress callback wiring in rebuild_index is missing:
_tick is defined but never used, so on_progress never receives phase updates.
Update rebuild_index to invoke _tick at the start of each rebuild stage—before
processing claims, pages, entities, and embeddings—so the documented phase
labels are actually emitted while keeping the rest of the rebuild logic
unchanged.
- Around line 629-652: _rebuild_embeddings is writing vectors through
index_db.index_embedding, but semantic search reads from embedding_index via
search_semantic, so the rebuild is targeting the wrong storage. Update
_rebuild_embeddings to use the same put_embedding path that writes to
embedding_index, keeping the existing batch loop and embedder.encode_batch flow
intact while replacing the final persistence call.
In `@src/vouch/index_db.py`:
- Around line 128-167: The rebuild path in index_db is currently backfilling
only the legacy embeddings table, while search_semantic() reads from
embedding_index, so a reset/rebuild leaves semantic search with no usable data.
Update _rebuild_embeddings() and the related index_embedding/search_embeddings
flow so the rebuild populates embedding_index as well, or explicitly remove
embeddings from the rebuild path if it is no longer used, keeping the live
search store and rebuild target aligned.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 16ae3082-59ae-4868-8bc1-b87023073c74
⛔ Files ignored due to path filters (6)
desktop/docs/screenshots/browse.pngis excluded by!**/*.pngdesktop/docs/screenshots/dashboard.pngis excluded by!**/*.pngdesktop/docs/screenshots/dual-solve.pngis excluded by!**/*.pngdesktop/docs/screenshots/review.pngis excluded by!**/*.pngdesktop/package-lock.jsonis excluded by!**/package-lock.jsondocs/banner.svgis excluded by!**/*.svg
📒 Files selected for processing (120)
.github/workflows/security-audit.yml.pre-commit-config.yaml.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/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/be7aec64b0fc803a33cb3d610f67ae95e636877db20231ef72440a7cbe6b69d2/content.vouch/sources/be7aec64b0fc803a33cb3d610f67ae95e636877db20231ef72440a7cbe6b69d2/meta.yamlCONTRIBUTING.mdREADME.mddesktop/.gitignoredesktop/CHANGELOG.mddesktop/LICENSEdesktop/README.mddesktop/docs/architecture.mddesktop/docs/reports/2026-06-26.mddesktop/electron-builder.ymldesktop/electron.vite.config.tsdesktop/package.jsondesktop/resources/vouch/.gitkeepdesktop/scripts/dev-with-vouch.tsdesktop/scripts/gen-methods.tsdesktop/src/catalog/backend.jsondesktop/src/catalog/methods.jsondesktop/src/main/http-client.tsdesktop/src/main/index.tsdesktop/src/main/ipc.tsdesktop/src/main/jsonl-client.tsdesktop/src/main/kb-store.tsdesktop/src/main/supervisor.tsdesktop/src/main/tray.tsdesktop/src/main/types.tsdesktop/src/main/vouch-locator.tsdesktop/src/preload/index.tsdesktop/src/renderer/index.htmldesktop/src/renderer/src/App.tsxdesktop/src/renderer/src/app.cssdesktop/src/renderer/src/components/Diff.tsxdesktop/src/renderer/src/components/Drawer.tsxdesktop/src/renderer/src/components/EmptyState.tsxdesktop/src/renderer/src/components/MethodCard.tsxdesktop/src/renderer/src/components/MethodForm.tsxdesktop/src/renderer/src/components/Placeholder.tsxdesktop/src/renderer/src/components/Rail.tsxdesktop/src/renderer/src/components/StatusBar.tsxdesktop/src/renderer/src/components/Topbar.tsxdesktop/src/renderer/src/components/controls/index.tsxdesktop/src/renderer/src/components/results/Cards.tsxdesktop/src/renderer/src/components/results/JsonTree.tsxdesktop/src/renderer/src/components/results/Renderers.tsxdesktop/src/renderer/src/components/results/ResultView.tsxdesktop/src/renderer/src/components/results/atoms.tsxdesktop/src/renderer/src/global.d.tsdesktop/src/renderer/src/lib/VouchContext.tsxdesktop/src/renderer/src/lib/client.tsdesktop/src/renderer/src/lib/format.tsdesktop/src/renderer/src/lib/useOnOpen.tsxdesktop/src/renderer/src/lib/useVouchEvents.tsdesktop/src/renderer/src/main.tsxdesktop/src/renderer/src/views/Dashboard.tsxdesktop/src/renderer/src/views/DualSolve.tsxdesktop/src/renderer/src/views/GenericView.tsxdesktop/src/renderer/src/views/Review.tsxdesktop/src/renderer/src/views/blurbs.tsdesktop/src/renderer/src/views/registry.tsxdesktop/src/shared/ipc.tsdesktop/src/shared/methods.gen.tsdesktop/src/shared/methods.types.tsdesktop/test/catalog.test.tsdesktop/test/controls.test.tsxdesktop/test/gen-methods.test.tsdesktop/test/http-client.test.tsdesktop/test/jsonl-client.test.tsdesktop/test/method-card.test.tsxdesktop/test/method-form.test.tsxdesktop/test/method-gate.test.tsdesktop/test/smoke-jsonl.tsdesktop/test/vouch-locator.test.tsdesktop/tsconfig.jsondesktop/tsconfig.node.jsondesktop/tsconfig.scripts.jsondesktop/tsconfig.web.jsondesktop/vitest.config.tsdocs/superpowers/specs/2026-07-06-review-ui-multi-kb-design.mdmigrations/README.mdspec/2026-05-21/README.mdspec/2026-05-21/SPEC.mdspec/2026-05-21/audit-vocabulary.mdspec/2026-05-21/methods.mdspec/2026-05-21/retrieval.mdspec/2026-05-21/review-gate.mdspec/2026-05-21/transports.mdspec/README.mdspec/audit-vocabulary.mdspec/methods.mdspec/retrieval.mdspec/review-gate.mdspec/transports.mdsrc/vouch/bundle.pysrc/vouch/cli.pysrc/vouch/context.pysrc/vouch/embeddings/base.pysrc/vouch/health.pysrc/vouch/index_db.pysrc/vouch/jsonl_server.pysrc/vouch/server.pysrc/vouch/sessions.pysrc/vouch/storage.pytests/embeddings/conftest.pytests/embeddings/test_integration.pytests/embeddings/test_search.pytests/test_cli.py
💤 Files with no reviewable changes (87)
- desktop/.gitignore
- spec/README.md
- spec/2026-05-21/review-gate.md
- desktop/src/renderer/src/views/registry.tsx
- desktop/docs/reports/2026-06-26.md
- spec/retrieval.md
- spec/2026-05-21/audit-vocabulary.md
- spec/2026-05-21/SPEC.md
- spec/transports.md
- desktop/test/catalog.test.ts
- desktop/src/renderer/src/views/Review.tsx
- desktop/CHANGELOG.md
- desktop/test/smoke-jsonl.ts
- desktop/test/gen-methods.test.ts
- desktop/src/renderer/src/views/GenericView.tsx
- desktop/tsconfig.json
- spec/methods.md
- desktop/src/renderer/src/components/Diff.tsx
- desktop/src/renderer/src/views/Dashboard.tsx
- desktop/test/jsonl-client.test.ts
- desktop/src/renderer/src/views/DualSolve.tsx
- desktop/src/main/ipc.ts
- desktop/electron.vite.config.ts
- desktop/src/main/supervisor.ts
- desktop/src/main/jsonl-client.ts
- desktop/LICENSE
- desktop/scripts/dev-with-vouch.ts
- desktop/src/renderer/index.html
- desktop/src/shared/methods.types.ts
- migrations/README.md
- desktop/scripts/gen-methods.ts
- desktop/package.json
- desktop/test/controls.test.tsx
- desktop/test/vouch-locator.test.ts
- desktop/src/renderer/src/components/EmptyState.tsx
- desktop/src/renderer/src/lib/format.ts
- desktop/tsconfig.web.json
- desktop/src/preload/index.ts
- spec/review-gate.md
- desktop/test/http-client.test.ts
- spec/2026-05-21/transports.md
- desktop/src/main/types.ts
- desktop/src/renderer/src/lib/useVouchEvents.ts
- desktop/vitest.config.ts
- desktop/README.md
- desktop/src/renderer/src/lib/useOnOpen.tsx
- spec/2026-05-21/README.md
- desktop/test/method-card.test.tsx
- desktop/src/shared/ipc.ts
- desktop/electron-builder.yml
- desktop/test/method-gate.test.ts
- desktop/src/renderer/src/components/Drawer.tsx
- desktop/src/renderer/src/App.tsx
- desktop/src/renderer/src/views/blurbs.ts
- desktop/src/renderer/src/lib/VouchContext.tsx
- desktop/tsconfig.node.json
- desktop/src/renderer/src/components/results/ResultView.tsx
- desktop/tsconfig.scripts.json
- desktop/src/renderer/src/main.tsx
- spec/2026-05-21/methods.md
- desktop/src/main/kb-store.ts
- desktop/src/renderer/src/app.css
- desktop/src/renderer/src/components/StatusBar.tsx
- desktop/src/renderer/src/components/Placeholder.tsx
- desktop/src/catalog/backend.json
- spec/audit-vocabulary.md
- desktop/src/renderer/src/components/results/atoms.tsx
- desktop/test/method-form.test.tsx
- desktop/src/main/tray.ts
- spec/2026-05-21/retrieval.md
- desktop/src/main/index.ts
- desktop/src/main/vouch-locator.ts
- desktop/src/catalog/methods.json
- desktop/src/renderer/src/components/MethodForm.tsx
- desktop/docs/architecture.md
- desktop/src/renderer/src/components/MethodCard.tsx
- desktop/src/renderer/src/components/controls/index.tsx
- desktop/src/renderer/src/global.d.ts
- desktop/src/shared/methods.gen.ts
- desktop/src/renderer/src/components/Topbar.tsx
- desktop/src/main/http-client.ts
- desktop/src/renderer/src/components/results/Renderers.tsx
- desktop/src/renderer/src/components/Rail.tsx
- desktop/src/renderer/src/components/results/Cards.tsx
- desktop/src/renderer/src/components/results/JsonTree.tsx
- .pre-commit-config.yaml
- desktop/src/renderer/src/lib/client.ts
| timeout-minutes: 15 | ||
| continue-on-error: true | ||
| steps: | ||
| - uses: actions/checkout@v4 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the workflow and nearby workflow settings.
sed -n '1,220p' .github/workflows/security-audit.yml
echo
echo "----"
# Look for token/permissions settings in this workflow and related workflows.
rg -n "persist-credentials|permissions:|checkout@v4|pip-audit|pull_request|workflow_dispatch|schedule" .github/workflows -SRepository: vouchdev/vouch
Length of output: 4157
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect packaging metadata to see what the editable install can execute.
sed -n '1,220p' pyproject.toml
echo
echo "----"
# Find any setup.py or build backend files that might run during install.
git ls-files | rg '(^|/)(setup\.py|setup\.cfg|pyproject\.toml|setup_hooks|build\.py)$' -nRepository: vouchdev/vouch
Length of output: 4149
Disable persisted checkout credentials.
actions/checkout stores the token in local git config by default. This workflow installs PR-controlled code, so leaving it there is unnecessary exposure; set persist-credentials: false.
🔧 Proposed fix
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false📝 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.
| - uses: actions/checkout@v4 | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false |
🧰 Tools
🪛 zizmor (1.26.1)
[warning] 27-27: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 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/security-audit.yml at line 27, The security-audit workflow
is using actions/checkout with default persisted credentials, which leaves the
token stored in local git config unnecessarily. Update the checkout step in the
workflow to disable credential persistence by setting persist-credentials to
false on the actions/checkout@v4 step so the PR-controlled code cannot reuse the
token.
Source: Linters/SAST tools
| {"cmd": "echo \"=== active gh account ===\" && gh api user --jq .login; echo \"=== is plind-junior now a PUBLIC member of vouchdev? ===\" && gh api orgs/vouchdev/public_members --jq '.[].login' 2>&1 | head -20; ec", "summary": "Ran: echo \"=== active gh account ===\" && gh api user --jq .login;", "tool": "Bash", "ts": 1783420221.0744834} | ||
| {"cmd": "echo \"=== publish ===\" && mcp-publisher publish 2>&1 | tail -15", "summary": "Command failed: echo \"=== publish ===\" && mcp-publisher publish 2>&1 | tail ", "tool": "Bash", "ts": 1783420292.7722068} | ||
| {"cmd": "echo \"=== look for mcp-publisher token files ===\" && find / -maxdepth 6 -name \"*mcp-publisher*\" -o -name \".mcpregistry*\" 2>/dev/null | head -20; echo \"=== any recently-modified dotfiles in likely spot", "summary": "Ran: echo \"=== look for mcp-publisher token files ===\" && find / ", "tool": "Bash", "ts": 1783420318.553565} | ||
| {"cmd": "python3 -c \"\nimport json, base64, time\nd = json.load(open('/home/a/.config/mcp-publisher/token.json'))\nprint('keys in token.json:', list(d.keys()))\ntok = d.get('token') or d.get('access_token') or ''\n", "summary": "Ran: python3 -c \"", "tool": "Bash", "ts": 1783420336.7329283} |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== file exists ==="
git ls-files -- .vouch/captures/1218169c-de25-48f4-bd93-70ff77d2fda2.jsonl
echo "=== nearby lines 60-80 ==="
sed -n '60,80p' .vouch/captures/1218169c-de25-48f4-bd93-70ff77d2fda2.jsonl | cat -n
echo "=== repo references to .vouch/captures ==="
rg -n --hidden --no-messages '\.vouch/captures|mcp-publisher|token\.json|public_members|gh api user' . || trueRepository: vouchdev/vouch
Length of output: 11653
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '68,71p' .vouch/captures/1218169c-de25-48f4-bd93-70ff77d2fda2.jsonl | cat -nRepository: vouchdev/vouch
Length of output: 1367
Scrub the auth/account probes from this capture. The gh auth switch commands and /home/a/.config/mcp-publisher/token.json path expose machine-specific account details; redact those lines before this log is committed or shared.
🤖 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 @.vouch/captures/1218169c-de25-48f4-bd93-70ff77d2fda2.jsonl around lines 68 -
71, Remove the auth/account probing from this capture by redacting the `gh`
account checks and any direct references to
`/home/a/.config/mcp-publisher/token.json`, since they expose machine-specific
identity and token location details. Keep the surrounding publish/debug context
in the JSONL entry, but replace those sensitive commands and paths with generic
redactions so the `mcp-publisher` capture can be safely shared. Use the existing
capture entries in this JSONL file as the place to scrub the sensitive lines
before commit.
| status: rejected | ||
| decided_at: '2026-07-07T09:30:38.823259Z' | ||
| decided_by: unknown-agent | ||
| decision_reason: 'yes' |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Replace the placeholder rejection reason.
decision_reason: 'yes' doesn't explain why this record was rejected, so the audit trail won't be useful later. Please replace it with a short human-readable reason, or omit the field if there isn't one yet.
🤖 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 @.vouch/decided/20260707-092531-154cb163.yaml around lines 42 - 45, The
rejection record uses a placeholder decision_reason value, which leaves the
audit trail unclear. Update the rejected entry in the YAML so the
decided_at/decided_by metadata remains intact but decision_reason is replaced
with a short human-readable explanation, or remove decision_reason entirely if
no reason is available yet.
| - /home/a/.claude/projects/-home-a-Dev-plind-junior-vouch/memory/MEMORY.md | ||
| - /home/a/.claude/projects/-home-a-Dev-plind-junior-vouch/memory/commit-scope-src-adapters.md | ||
| - /tmp/claude-1000/-home-a-Dev-plind-junior-vouch/e5ead202-24c2-4916-b59c-ddb4c123d63f/scratchpad/merge_hook.py |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Redact the local-path leak from the session snapshot.
These entries expose a home directory and temp-path layout in a committed artifact, and the same leak is also present in .vouch/decided/20260707-093005-05345f47.yaml. Replace them with repo-relative paths or placeholders before committing.
Suggested redaction
- /home/a/.claude/projects/-home-a-Dev-plind-junior-vouch/memory/MEMORY.md
- /home/a/.claude/projects/-home-a-Dev-plind-junior-vouch/memory/commit-scope-src-adapters.md
- /tmp/claude-1000/-home-a-Dev-plind-junior-vouch/e5ead202-24c2-4916-b59c-ddb4c123d63f/scratchpad/merge_hook.py
+ [redacted]/memory/MEMORY.md
+ [redacted]/memory/commit-scope-src-adapters.md
+ [redacted]/scratchpad/merge_hook.py📝 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.
| - /home/a/.claude/projects/-home-a-Dev-plind-junior-vouch/memory/MEMORY.md | |
| - /home/a/.claude/projects/-home-a-Dev-plind-junior-vouch/memory/commit-scope-src-adapters.md | |
| - /tmp/claude-1000/-home-a-Dev-plind-junior-vouch/e5ead202-24c2-4916-b59c-ddb4c123d63f/scratchpad/merge_hook.py | |
| [redacted]/memory/MEMORY.md | |
| [redacted]/memory/commit-scope-src-adapters.md | |
| [redacted]/scratchpad/merge_hook.py |
🤖 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 @.vouch/pages/session-what-are-the-non-development-related-files-and-folde.md
around lines 36 - 38, The session snapshot includes absolute local paths that
leak the developer’s home and temp directory layout. Redact these entries in the
session markdown and the matching `.vouch/decided/...yaml` artifact by replacing
them with repo-relative paths or neutral placeholders, using the same path list
entries shown in the snapshot.
| def rebuild_index(store: KBStore, *, on_progress: Callable[[str], None] | None = None) -> dict: | ||
| """Drop and rebuild state.db from the durable files. Idempotent. | ||
|
|
||
| `on_progress`, if given, is called with a short phase label ("claims", | ||
| "pages", "entities", "embeddings") as each stage starts — for CLI | ||
| progress display. It never affects the result. | ||
| """ | ||
|
|
||
| def _tick(phase: str) -> None: | ||
| if on_progress is not None: | ||
| on_progress(phase) | ||
|
|
||
| # Detect a stale embedding-model identity before reset() wipes the meta. | ||
| try: | ||
| from . import audit | ||
| from .embeddings.migration import detect_mismatch | ||
|
|
||
| m = detect_mismatch(store.kb_dir) | ||
| if m is not None: | ||
| audit.log_event( | ||
| store.kb_dir, | ||
| event="embedding.model_mismatch", | ||
| actor="vouch-health", | ||
| object_ids=[], | ||
| data=m, | ||
| ) | ||
| except ImportError: | ||
| pass | ||
| index_db.reset(store.kb_dir) | ||
| with index_db.open_db(store.kb_dir) as conn: | ||
| for c in store.list_claims(): | ||
| index_db.index_claim( | ||
| conn, id=c.id, text=c.text, | ||
| type=c.type.value, status=c.status.value, tags=c.tags, | ||
| conn, | ||
| id=c.id, | ||
| text=c.text, | ||
| type=c.type.value, | ||
| status=c.status.value, | ||
| tags=c.tags, | ||
| ) | ||
| for p in store.list_pages(): | ||
| index_db.index_page( | ||
| conn, id=p.id, title=p.title, body=p.body, | ||
| type=p.type.value, tags=p.tags, | ||
| conn, | ||
| id=p.id, | ||
| title=p.title, | ||
| body=p.body, | ||
| type=p.type, | ||
| tags=p.tags, | ||
| ) | ||
| for e in store.list_entities(): | ||
| index_db.index_entity( | ||
| conn, id=e.id, name=e.name, description=e.description, | ||
| type=e.type.value, aliases=e.aliases, | ||
| conn, | ||
| id=e.id, | ||
| name=e.name, | ||
| description=e.description, | ||
| type=e.type.value, | ||
| aliases=e.aliases, | ||
| ) | ||
| _rebuild_embeddings(store) | ||
| return index_db.stats(store.kb_dir) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
_tick is defined but never invoked — on_progress never fires.
The helper is declared and documented to emit "claims"/"pages"/"entities"/"embeddings" phase labels, but no stage in the rebuild body actually calls it, so the CLI progress callback is dead code.
🐛 Proposed fix to wire up the phase ticks
index_db.reset(store.kb_dir)
with index_db.open_db(store.kb_dir) as conn:
+ _tick("claims")
for c in store.list_claims():
index_db.index_claim(
conn,
id=c.id,
text=c.text,
type=c.type.value,
status=c.status.value,
tags=c.tags,
)
+ _tick("pages")
for p in store.list_pages():
index_db.index_page(
conn,
id=p.id,
title=p.title,
body=p.body,
type=p.type,
tags=p.tags,
)
+ _tick("entities")
for e in store.list_entities():
index_db.index_entity(
conn,
id=e.id,
name=e.name,
description=e.description,
type=e.type.value,
aliases=e.aliases,
)
+ _tick("embeddings")
_rebuild_embeddings(store)
return index_db.stats(store.kb_dir)📝 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.
| def rebuild_index(store: KBStore, *, on_progress: Callable[[str], None] | None = None) -> dict: | |
| """Drop and rebuild state.db from the durable files. Idempotent. | |
| `on_progress`, if given, is called with a short phase label ("claims", | |
| "pages", "entities", "embeddings") as each stage starts — for CLI | |
| progress display. It never affects the result. | |
| """ | |
| def _tick(phase: str) -> None: | |
| if on_progress is not None: | |
| on_progress(phase) | |
| # Detect a stale embedding-model identity before reset() wipes the meta. | |
| try: | |
| from . import audit | |
| from .embeddings.migration import detect_mismatch | |
| m = detect_mismatch(store.kb_dir) | |
| if m is not None: | |
| audit.log_event( | |
| store.kb_dir, | |
| event="embedding.model_mismatch", | |
| actor="vouch-health", | |
| object_ids=[], | |
| data=m, | |
| ) | |
| except ImportError: | |
| pass | |
| index_db.reset(store.kb_dir) | |
| with index_db.open_db(store.kb_dir) as conn: | |
| for c in store.list_claims(): | |
| index_db.index_claim( | |
| conn, id=c.id, text=c.text, | |
| type=c.type.value, status=c.status.value, tags=c.tags, | |
| conn, | |
| id=c.id, | |
| text=c.text, | |
| type=c.type.value, | |
| status=c.status.value, | |
| tags=c.tags, | |
| ) | |
| for p in store.list_pages(): | |
| index_db.index_page( | |
| conn, id=p.id, title=p.title, body=p.body, | |
| type=p.type.value, tags=p.tags, | |
| conn, | |
| id=p.id, | |
| title=p.title, | |
| body=p.body, | |
| type=p.type, | |
| tags=p.tags, | |
| ) | |
| for e in store.list_entities(): | |
| index_db.index_entity( | |
| conn, id=e.id, name=e.name, description=e.description, | |
| type=e.type.value, aliases=e.aliases, | |
| conn, | |
| id=e.id, | |
| name=e.name, | |
| description=e.description, | |
| type=e.type.value, | |
| aliases=e.aliases, | |
| ) | |
| _rebuild_embeddings(store) | |
| return index_db.stats(store.kb_dir) | |
| def rebuild_index(store: KBStore, *, on_progress: Callable[[str], None] | None = None) -> dict: | |
| """Drop and rebuild state.db from the durable files. Idempotent. | |
| `on_progress`, if given, is called with a short phase label ("claims", | |
| "pages", "entities", "embeddings") as each stage starts — for CLI | |
| progress display. It never affects the result. | |
| """ | |
| def _tick(phase: str) -> None: | |
| if on_progress is not None: | |
| on_progress(phase) | |
| # Detect a stale embedding-model identity before reset() wipes the meta. | |
| try: | |
| from . import audit | |
| from .embeddings.migration import detect_mismatch | |
| m = detect_mismatch(store.kb_dir) | |
| if m is not None: | |
| audit.log_event( | |
| store.kb_dir, | |
| event="embedding.model_mismatch", | |
| actor="vouch-health", | |
| object_ids=[], | |
| data=m, | |
| ) | |
| except ImportError: | |
| pass | |
| index_db.reset(store.kb_dir) | |
| with index_db.open_db(store.kb_dir) as conn: | |
| _tick("claims") | |
| for c in store.list_claims(): | |
| index_db.index_claim( | |
| conn, | |
| id=c.id, | |
| text=c.text, | |
| type=c.type.value, | |
| status=c.status.value, | |
| tags=c.tags, | |
| ) | |
| _tick("pages") | |
| for p in store.list_pages(): | |
| index_db.index_page( | |
| conn, | |
| id=p.id, | |
| title=p.title, | |
| body=p.body, | |
| type=p.type, | |
| tags=p.tags, | |
| ) | |
| _tick("entities") | |
| for e in store.list_entities(): | |
| index_db.index_entity( | |
| conn, | |
| id=e.id, | |
| name=e.name, | |
| description=e.description, | |
| type=e.type.value, | |
| aliases=e.aliases, | |
| ) | |
| _tick("embeddings") | |
| _rebuild_embeddings(store) | |
| return index_db.stats(store.kb_dir) |
🤖 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/health.py` around lines 568 - 626, The progress callback wiring in
rebuild_index is missing: _tick is defined but never used, so on_progress never
receives phase updates. Update rebuild_index to invoke _tick at the start of
each rebuild stage—before processing claims, pages, entities, and embeddings—so
the documented phase labels are actually emitted while keeping the rest of the
rebuild logic unchanged.
| def _rebuild_embeddings(store: KBStore) -> None: | ||
| try: | ||
| from .embeddings import get_embedder | ||
|
|
||
| embedder = get_embedder() | ||
| except Exception: | ||
| return | ||
| with index_db.open_db(store.kb_dir) as conn: | ||
| texts: list[tuple[str, str, str]] = [] | ||
| for c in store.list_claims(): | ||
| texts.append(("claim", c.id, c.text)) | ||
| for p in store.list_pages(): | ||
| texts.append(("page", p.id, f"{p.title} {p.body}")) | ||
| for e in store.list_entities(): | ||
| texts.append(("entity", e.id, f"{e.name} {e.description or ''}")) | ||
| if not texts: | ||
| return | ||
| batch_size = 64 | ||
| for i in range(0, len(texts), batch_size): | ||
| batch = texts[i : i + batch_size] | ||
| vecs = embedder.encode_batch([t[2] for t in batch]) | ||
| for (kind, eid, _), row in zip(batch, vecs, strict=True): | ||
| index_db.index_embedding(conn, kind=kind, id=eid, vec=row.tolist()) | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Which table does semantic search / put_embedding read/write, vs index_embedding?
ast-grep outline src/vouch/index_db.py --items all
rg -nP 'FROM\s+(embeddings|embedding_index)\b|INTO\s+(embeddings|embedding_index)\b' src/vouch/index_db.py
rg -nP 'def\s+(search_semantic|put_embedding|index_embedding|reset)\b' -A15 src/vouch/index_db.pyRepository: vouchdev/vouch
Length of output: 5070
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect schema and the full read/write paths for both embedding tables.
sed -n '24,90p;120,180p;315,470p' src/vouch/index_db.py
# Find all call sites/usages to understand intended behavior.
rg -n '\b(index_embedding|put_embedding|get_embedding|search_embeddings|search_embedding|search_semantic)\b' src/vouch -A3 -B3
# Inspect the health rebuild path in context.
sed -n '600,690p' src/vouch/health.pyRepository: vouchdev/vouch
Length of output: 22119
Write rebuilt vectors to embedding_index
_rebuild_embeddings calls index_db.index_embedding, which writes to embeddings, while search_semantic reads from embedding_index. A full reindex will repopulate the wrong table and leave semantic search empty until later artifact writes backfill it. Use the put_embedding path here instead.
🤖 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/health.py` around lines 629 - 652, _rebuild_embeddings is writing
vectors through index_db.index_embedding, but semantic search reads from
embedding_index via search_semantic, so the rebuild is targeting the wrong
storage. Update _rebuild_embeddings to use the same put_embedding path that
writes to embedding_index, keeping the existing batch loop and
embedder.encode_batch flow intact while replacing the final persistence call.
Source: Linters/SAST tools
| "DELETE FROM prov_edges;" | ||
| "DELETE FROM index_meta WHERE key LIKE 'embedding_%';" | ||
| "DELETE FROM index_meta WHERE key LIKE 'prov_%';" | ||
| ) | ||
|
|
||
|
|
||
| def index_embedding(conn: sqlite3.Connection, *, kind: str, id: str, | ||
| vec: list[float]) -> None: | ||
| conn.execute( | ||
| "INSERT OR REPLACE INTO embeddings (kind, id, vec) VALUES (?, ?, ?)", | ||
| (kind, id, json.dumps(vec)), | ||
| ) | ||
|
|
||
|
|
||
| def search_embeddings(kb_dir: Path, query_vec: list[float], *, | ||
| limit: int = 10 | ||
| ) -> list[tuple[str, str, str, float]]: | ||
| """Return (kind, id, snippet, cosine_score) via brute-force NumPy scan.""" | ||
| import numpy as np # type: ignore[import-not-found] | ||
| out: list[tuple[str, str, str, float]] = [] | ||
| if not query_vec: | ||
| return out | ||
| q = np.array(query_vec, dtype=np.float32) | ||
| q_norm = np.linalg.norm(q) | ||
| if q_norm == 0.0: | ||
| return out | ||
| q = q / q_norm | ||
| with open_db(kb_dir) as conn: | ||
| rows = conn.execute( | ||
| "SELECT kind, id, vec FROM embeddings" | ||
| ).fetchall() | ||
| for kind, eid, vec_json in rows: | ||
| v = np.array(json.loads(vec_json), dtype=np.float32) | ||
| if v.ndim != 1 or v.shape[0] != q.shape[0]: | ||
| continue | ||
| score = float(np.dot(q, v)) | ||
| snippet = _snippet_for(kb_dir, kind, eid) | ||
| out.append((kind, eid, snippet, score)) | ||
| out.sort(key=lambda x: x[3], reverse=True) | ||
| return out[:limit] |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
echo "=== search_embedding (singular) impl ==="
rg -nP -A25 'def search_embedding\b' src/vouch/index_db.py
echo "=== callers of search_embeddings (plural) ==="
rg -n 'search_embeddings' src/vouch
echo "=== reset() body ==="
rg -nP -A20 'def reset\b' src/vouch/index_db.py
echo "=== table read/write sites ==="
rg -n "INTO embedding_index|INTO embeddings |FROM embedding_index|FROM embeddings\b" src/vouchRepository: vouchdev/vouch
Length of output: 3528
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== _rebuild_embeddings and related callers ==="
rg -nP -A40 'def _rebuild_embeddings\b|_rebuild_embeddings\(' src/vouch/health.py src/vouch/index_db.py
echo "=== search_semantic implementation ==="
rg -nP -A40 'def search_semantic\b' src/vouch/index_db.py
echo "=== embedding table schema references ==="
rg -n "CREATE TABLE .*embeddings|CREATE TABLE .*embedding_index|embeddings\b" src/vouch
echo "=== any callers of search_embeddings or index_embedding outside index_db.py ==="
rg -n "search_embeddings\(|index_embedding\(" src/vouchRepository: vouchdev/vouch
Length of output: 11942
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== index_db.py table definitions and search helpers ==="
sed -n '1,220p' src/vouch/index_db.py
echo "=== search_embedding implementation tail ==="
sed -n '400,470p' src/vouch/index_db.py
echo "=== health.py orphan embedding logic ==="
sed -n '530,580p' src/vouch/health.pyRepository: vouchdev/vouch
Length of output: 11994
Rebuild should target embedding_index
_rebuild_embeddings() is backfilling the legacy embeddings table, but search_semantic() still reads embedding_index. After reset(), a rebuild leaves the live semantic-search index empty; either populate embedding_index here or clear embeddings out of the rebuild path as well.
🧰 Tools
🪛 ast-grep (0.44.1)
[info] 137-137: use jsonify instead of json.dumps for JSON output
Context: json.dumps(vec)
Note: [CWE-116] Improper Encoding or Escaping of Output.
(use-jsonify)
🤖 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/index_db.py` around lines 128 - 167, The rebuild path in index_db
is currently backfilling only the legacy embeddings table, while
search_semantic() reads from embedding_index, so a reset/rebuild leaves semantic
search with no usable data. Update _rebuild_embeddings() and the related
index_embedding/search_embeddings flow so the rebuild populates embedding_index
as well, or explicitly remove embeddings from the rebuild path if it is no
longer used, keeping the live search store and rebuild target aligned.
What changed
Why
What might break
VEP
Tests
make checkpasses locally (lint + mypy + pytest)CHANGELOG.mdupdated under## [Unreleased]Summary by CodeRabbit
New Features
Bug Fixes
Documentation