Skip to content

Commit 71e1712

Browse files
authored
Merge pull request #16 from ngallodev/feat/output-token-budget
Feat/output token budget
2 parents f48aa9b + 9a962d1 commit 71e1712

214 files changed

Lines changed: 22541 additions & 3525 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/_lint.yml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ jobs:
7171
- name: Install LLVM 21 (pinned analyzer toolchain)
7272
run: |
7373
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
74-
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-22 main" | sudo tee /etc/apt/sources.list.d/llvm-22.list
74+
echo "deb http://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" | sudo tee /etc/apt/sources.list.d/llvm-21.list
7575
sudo apt-get update
76-
sudo apt-get install -y clang-tidy-22
76+
sudo apt-get install -y clang-tidy-21
7777
7878
- name: Memory-analyzer gate
79-
run: scripts/ci/lint-mem.sh clang-tidy-22
79+
run: scripts/ci/lint-mem.sh clang-tidy-21

‎.github/workflows/_test.yml‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,32 @@ jobs:
119119
if: startsWith(matrix.os, 'ubuntu')
120120
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache
121121

122+
# Ubuntu 23.10+ ships kernel.apparmor_restrict_unprivileged_userns=1, which
123+
# blocks unshare(CLONE_NEWUSER) for unprivileged users. Without this, the
124+
# #1830 single-uid-userns smoke test SKIPS on every 24.04 leg, and its only
125+
# real-namespace coverage lived on the two ubuntu-22.04 legs -- which are
126+
# broad-matrix only (never PR CI) and which GitHub retires on 2027-04-17.
127+
# A deliberate relaxation of a security check was therefore guarded by a
128+
# test that ran in no PR and was on a clock. Hosted runners have
129+
# passwordless sudo, so one sysctl puts it on the CORE matrix instead.
130+
# Best-effort: a runner that refuses simply returns to skipping.
131+
- name: Permit unprivileged user namespaces (Ubuntu)
132+
if: startsWith(matrix.os, 'ubuntu')
133+
run: |
134+
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 || \
135+
echo "could not lift the userns restriction; namespace tests will skip"
136+
# Then SAY whether it worked. The runner prints per-shard aggregates
137+
# ("N passed, M skipped") and never individual test names, so a
138+
# namespace test that silently returns to skipping is invisible in a
139+
# green log -- the same false assurance that let #1830 ship with a
140+
# test that never executed. This line is the evidence. Probed exactly
141+
# as the test does it: unprivileged, no sudo.
142+
if unshare -U -r true 2>/dev/null; then
143+
echo "CBM_USERNS_AVAILABLE=yes"
144+
else
145+
echo "CBM_USERNS_AVAILABLE=no (namespace-dependent tests will skip)"
146+
fi
147+
122148
- name: Install ccache (macOS)
123149
if: startsWith(matrix.os, 'macos')
124150
run: command -v ccache >/dev/null 2>&1 || brew install ccache
@@ -324,8 +350,14 @@ jobs:
324350
steps:
325351
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
326352

353+
# Pinned: the unversioned `llvm` formula tracks Homebrew stable, so the
354+
# leg's compiler changed under us when stable moved 22 -> 23 (a new
355+
# -Wunused-but-set-global under -Werror broke the build on whichever
356+
# runner drew the 23 bottle, and passed on the ones that drew 22). A
357+
# test verdict must be a function of the code, not of the image snapshot;
358+
# moving to a newer major is a deliberate one-line bump here.
327359
- name: Install LSan-capable toolchain
328-
run: brew install llvm ccache
360+
run: brew install llvm@22 ccache
329361

330362
- name: Compiler cache (content-verified, restore)
331363
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
@@ -340,7 +372,7 @@ jobs:
340372
env:
341373
ASAN_OPTIONS: "detect_leaks=1:halt_on_error=1"
342374
run: |
343-
LLVM_PREFIX="$(brew --prefix llvm)"
375+
LLVM_PREFIX="$(brew --prefix llvm@22)"
344376
scripts/test.sh CC="$LLVM_PREFIX/bin/clang" CXX="$LLVM_PREFIX/bin/clang++"
345377
346378
- name: Compiler cache (save)

‎.github/workflows/pages.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,4 @@ jobs:
4141
with:
4242
path: ./docs
4343
- id: deployment
44-
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
44+
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1

‎.github/workflows/release.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ jobs:
288288
git tag "$VERSION" "$GITHUB_SHA"
289289
git push origin "refs/tags/$VERSION"
290290
291-
- uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v2
291+
- uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
292292
with:
293293
tag_name: ${{ inputs.version }}
294294
draft: true

‎Makefile.cbm‎

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,11 @@ KOTLIN_DEDUP_TEST_DEFINE = -DCBM_KOTLIN_DEDUP_TEST_API=1
100100
CALL_REFERENCE_LOOKUP_TEST_DEFINE = -DCBM_CALL_REFERENCE_LOOKUP_TEST_API=1
101101
INCREMENTAL_TEST_DEFINE = -DCBM_INCREMENTAL_TEST_API=1
102102
CFLAGS_TEST_EXTRA ?=
103+
COVERAGE_MARKER_TEST_DEFINE = -DCBM_COVERAGE_MARKER_TEST_API=1
103104
CFLAGS_TEST = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(SANITIZED_DEFINE) \
104105
$(KOTLIN_DEDUP_TEST_DEFINE) $(CALL_REFERENCE_LOOKUP_TEST_DEFINE) \
105-
$(INCREMENTAL_TEST_DEFINE) -g -O1 $(SANITIZE) $(CFLAGS_TEST_EXTRA)
106+
$(INCREMENTAL_TEST_DEFINE) $(COVERAGE_MARKER_TEST_DEFINE) -g -O1 $(SANITIZE) \
107+
$(CFLAGS_TEST_EXTRA)
106108
CXXFLAGS_TEST = $(CXXFLAGS_COMMON) $(SANITIZED_DEFINE) -g -O1 $(SANITIZE) $(CXX_STDLIB_FLAGS)
107109

108110
# TSan (can't combine with ASan)
@@ -120,6 +122,7 @@ TSAN_SANITIZE = -fsanitize=thread -fno-omit-frame-pointer
120122
# macro of ours.
121123
CFLAGS_TSAN = $(CFLAGS_COMMON) $(EDITOR_TEST_DEFINES) $(KOTLIN_DEDUP_TEST_DEFINE) \
122124
$(CALL_REFERENCE_LOOKUP_TEST_DEFINE) $(INCREMENTAL_TEST_DEFINE) \
125+
$(COVERAGE_MARKER_TEST_DEFINE) \
123126
-DCBM_SANITIZED_BUILD=1 -g -O1 $(TSAN_SANITIZE)
124127
CXXFLAGS_TSAN = $(CXXFLAGS_COMMON) -DCBM_SANITIZED_BUILD=1 -g -O1 \
125128
$(TSAN_SANITIZE)
@@ -183,10 +186,35 @@ endif
183186
# ~259 MB of tree-sitter parse tables was mapped executable at runtime while the
184187
# amd64 build of the same source mapped them R only. That is a large ROP gadget
185188
# surface for data that is never executed, and it is invisible to section flags.
189+
#
190+
# -z relro + -z now finish the same job on the OTHER writable-but-shouldn't-be
191+
# region: the GOT. Both are free at runtime here (a static binary resolves
192+
# everything at link time, so there is no lazy binding left to pay for), and
193+
# they are added because the SHIPPED artifact measurably lacked the property --
194+
# not because the linker accepts the flags. Measured on this binary, built the
195+
# way .github/workflows/_build.yml builds the Linux release (STATIC=1, gcc 13.3
196+
# / GNU ld 2.42, Ubuntu 24.04 aarch64); PT_GNU_RELRO ends at 0x11DA0000 in both:
197+
#
198+
# without -z now: .got 0x11d9ec20+0x13c8, .got.plt 0x11d9ffe8+0x40
199+
# -> .got.plt ends at 0x11DA0028, i.e. 40 bytes PAST the
200+
# window: those GOT slots stayed writable for the whole
201+
# process lifetime, in every Linux binary we have shipped
202+
# with -z now: .got.plt folded into .got (0x11d9ec20+0x13c8, ends
203+
# 0x11D9FFE8) -> the whole GOT is inside the window and is
204+
# re-mapped read-only after startup
205+
#
206+
# -z relro is already this toolchain's default and changes nothing here; it is
207+
# named anyway so the property stops depending on one distro's spec file (the
208+
# musl/portable and glibc-floor images are different toolchains, and a default
209+
# is not a guarantee). A1c/A1d in scripts/ci/check-binary-composition.sh assert
210+
# the OUTCOME on the produced binary, because a flag the compiler accepts is
211+
# not evidence that the artifact gained anything -- the same reason A1 exists
212+
# next to the .note.GNU-stack annotation.
186213
ELF_HARDENING_FLAGS :=
187214
ifeq ($(IS_LINUX),yes)
188215
ifneq ($(IS_MINGW),yes)
189-
ELF_HARDENING_FLAGS := -Wl,-z,noexecstack -Wl,-z,separate-code
216+
ELF_HARDENING_FLAGS := -Wl,-z,noexecstack -Wl,-z,separate-code \
217+
-Wl,-z,relro -Wl,-z,now
190218
endif
191219
endif
192220

@@ -225,6 +253,7 @@ FOUNDATION_SRCS = \
225253
src/foundation/compat_fs.c \
226254
src/foundation/compat_regex.c \
227255
src/foundation/mem.c \
256+
src/foundation/mem_core.c \
228257
src/foundation/diagnostics.c \
229258
src/foundation/profile.c \
230259
src/foundation/dump_verify.c \
@@ -253,6 +282,8 @@ EXTRACTION_SRCS = \
253282
$(CBM_DIR)/extract_k8s.c \
254283
$(CBM_DIR)/extract_dbt.c \
255284
$(CBM_DIR)/helpers.c \
285+
$(CBM_DIR)/result_compact.c \
286+
$(CBM_DIR)/result_spill.c \
256287
$(CBM_DIR)/lang_specs.c \
257288
$(CBM_DIR)/macro_table.c \
258289
$(CBM_DIR)/iris_export_xml.c \
@@ -304,7 +335,7 @@ STORE_SRCS = src/store/store.c
304335
# Cypher module (new)
305336
CYPHER_SRCS = src/cypher/cypher.c
306337

307-
OPERATION_SRCS = src/operations/registry.c src/operations/json_args.c src/operations/reliability_events.c src/operations/execute.c src/operations/result_wire.c src/operations/tool_catalog.c src/operations/index_admission.c src/operations/store_host.c src/operations/session_state.c src/operations/mutation.c src/operations/adr.c src/operations/cross_repo.c src/operations/index.c src/operations/project_arg.c src/operations/index_supervisor.c src/operations/command_runner.c src/operations/read.c src/operations/coverage.c src/operations/snippet.c src/operations/search.c src/operations/trace.c src/operations/trace_ingest.c src/operations/schema.c src/operations/query.c src/operations/architecture.c src/operations/changes.c src/operations/source_search.c src/operations/file_outline.c src/operations/compare.c src/operations/compact_out.c
338+
OPERATION_SRCS = src/operations/registry.c src/operations/json_args.c src/operations/reliability_events.c src/operations/execute.c src/operations/result_wire.c src/operations/output_budget.c src/operations/tool_catalog.c src/operations/index_admission.c src/operations/store_host.c src/operations/session_state.c src/operations/mutation.c src/operations/adr.c src/operations/cross_repo.c src/operations/index.c src/operations/project_arg.c src/operations/index_supervisor.c src/operations/command_runner.c src/operations/read.c src/operations/coverage.c src/operations/snippet.c src/operations/search.c src/operations/trace.c src/operations/trace_ingest.c src/operations/schema.c src/operations/query.c src/operations/architecture.c src/operations/changes.c src/operations/source_search.c src/operations/file_outline.c src/operations/compare.c src/operations/compact_out.c
308339

309340
# Shared daemon coordination and local transport
310341
DAEMON_SRCS = \
@@ -684,6 +715,7 @@ TEST_REPRO_SRCS = \
684715
tests/repro/repro_issue787.c \
685716
tests/repro/repro_issue842.c \
686717
tests/repro/repro_issue964.c \
718+
tests/repro/repro_issue1692.c \
687719
tests/repro/repro_invariant_calls.c \
688720
tests/repro/repro_invariant_graph.c \
689721
tests/repro/repro_invariant_breadth.c \
@@ -1182,8 +1214,17 @@ lint-no-suppress:
11821214
lint: lint-tidy lint-cppcheck lint-format lint-no-suppress
11831215
@echo "=== All linters passed ==="
11841216

1217+
# Memory-core linter: memory is allocated through src/foundation/mem_core.h,
1218+
# not through raw malloc/calloc/realloc/free/strdup. A checked-in baseline
1219+
# (scripts/memory-core-baseline.txt) records each file's raw-site count and the
1220+
# gate goes red the moment any file grows. Files only ever go down; lower the
1221+
# baseline line in the same change that migrates the file.
1222+
lint-memory-core:
1223+
@echo "=== memory-core linter ==="
1224+
@python3 scripts/lint-memory-core.py
1225+
11851226
# CI linters (no clang-tidy — platform-dependent, enforced locally via pre-commit)
1186-
lint-ci: lint-cppcheck lint-format lint-no-suppress
1227+
lint-ci: lint-cppcheck lint-format lint-no-suppress lint-memory-core
11871228
@echo "=== CI linters passed ==="
11881229

11891230
# ── Local memory-diagnostic lanes (not PR-CI gates by decision: the diag

‎README.md‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,7 @@ Design rules:
221221

222222
- stdout is reserved for command results;
223223
- progress and diagnostics belong on stderr;
224+
- `cli --progress` forces lifecycle progress when stderr is redirected, `cli --quiet` disables automatic terminal progress and ordinary diagnostics while keeping errors (it cannot be combined with `--progress` or `cli --verbose`), and `cli --verbose` includes routine informational logs;
224225
- errors produce non-zero exit codes;
225226
- canonical JSON hides the historical transport envelope;
226227
- result ordering/pagination remain explicit where the underlying operation supports them.
@@ -247,6 +248,18 @@ The upstream indexing engine is intentionally preserved through the CLI-first mi
247248

248249
The first vertical slice does **not** rewrite the graph schema, parser pipeline, store format, or existing indexes.
249250

251+
### Shared graph artifact
252+
253+
`.codebase-memory/graph.db.zst` is an optional zstd-compressed snapshot of the graph that sits next to the source. `codebase-memory-cli index` writes or refreshes it (the watcher refreshes it with a faster, lower-compression tier), and a first `index` on a clone with no local database imports it before running incremental indexing. A `.codebase-memory/.gitattributes` line with `merge=ours` is created on first export so concurrent edits do not conflict on the binary file.
254+
255+
- **Commit it deliberately.** The artifact is rewritten on every index, including the watcher's fast tier, and git stores each rewrite as a full new blob. Committing every refresh turns a 20 MB file into gigabytes of history. Pick a cadence (a release, a milestone, a nightly job) rather than committing every save.
256+
- **Git LFS, if it must move on every commit.** Track it from the repo-root `.gitattributes` and leave the auto-created `.codebase-memory/.gitattributes` in place; the nearer file keeps supplying `merge=ours`, and only `filter` comes from the root:
257+
```gitattributes
258+
.codebase-memory/graph.db.zst filter=lfs diff=lfs merge=lfs -text
259+
```
260+
Track only the `.zst`; `artifact.json` is small and carries the schema version. The attribute applies to future commits only, so a repo that already has the blobs in history needs `git-filter-repo` first. GitHub meters LFS storage and bandwidth, and every teammate needs `git lfs install`; without it the checkout leaves a pointer file, the integrity-checked import refuses it, and they fall back to a full reindex.
261+
- **Optional.** Add `.codebase-memory/` to `.gitignore` if everyone should reindex from scratch.
262+
250263
## Configuration
251264

252265
Common commands:
@@ -267,7 +280,7 @@ Important environment variables include:
267280
| `CBM_ALLOWED_ROOT` | Constrain permissible indexing roots. |
268281
| `CBM_RUNTIME_DIR` | Override the secure local coordination rendezvous parent. |
269282
| `CBM_WORKERS` | Override indexing worker count. |
270-
| `CBM_LOG_LEVEL` | Control runtime logging. |
283+
| `CBM_LOG_LEVEL` | Control runtime logging. One-shot CLI and hook commands default to `warn`; the detached daemon and supervised index workers default to `info`. |
271284

272285
See [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md) for the full reference.
273286

‎docs/CONFIGURATION.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ These environment variables affect runtime behavior:
133133
| `CBM_CACHE_DIR` | `~/.cache/codebase-memory-cli` | Override the cache directory used for indexes, `_config.db`, and UI `config.json`. |
134134
| `CBM_DIAGNOSTICS` | `false` | Enable periodic `snapshot.json` and retained `trajectory.ndjson` below a fresh owner-private directory in the system temp directory. The daemon records the randomized paths in the `diagnostics.start` discovery record (a single JSON line) in `${CBM_CACHE_DIR}/logs/cbm-daemon.log`; that one record is emitted even when `CBM_LOG_LEVEL` suppresses ordinary logging, so the paths always remain discoverable. |
135135
| `CBM_DOWNLOAD_URL` | GitHub releases | Override the update download URL. |
136-
| `CBM_LOG_LEVEL` | `info` | Set the log level to `debug`, `info`, `warn`, `error`, or `none` (or `0`-`4`). One-shot CLI messages use that command's stderr; retained detached-runtime events use `${CBM_CACHE_DIR}/logs/cbm-daemon.log`. |
136+
| `CBM_LOG_LEVEL` | role-aware | Set the log level to `debug`, `info`, `warn`, `error`, or `none` (or `0`-`4`). Thin CLI/hook frontends default to `warn`; the detached daemon and supervised index workers default to `info`. Physical workers retain INFO liveness records because their private logs drive the supervisor's no-progress timeout. Frontend messages use that command's stderr; detached daemon events use `${CBM_CACHE_DIR}/logs/cbm-daemon.log`. |
137137
| `CBM_RUNTIME_DIR` | `%LOCALAPPDATA%` (Windows), `/private/tmp` (macOS), `/tmp` (other) | Parent directory for the daemon/CLI rendezvous directory, which CBM creates inside it as `cbm-daemon-<uid>` (`cbm-daemon-<key>` on Windows). When an explicitly configured parent is missing, CBM creates it owner-private before use; existing parents are never modified and must pass the private-directory check. Set it when the default ancestry cannot pass that check — see below. `CBM_CACHE_DIR` does **not** move the rendezvous. |
138138
| `CBM_WORKERS` | auto-detected | Override the indexing worker count. |
139139

‎graph-ui/src/App.test.tsx‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/* @vitest-environment jsdom */
2+
import "@testing-library/jest-dom/vitest";
3+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
4+
import { afterEach, describe, expect, it, vi } from "vitest";
5+
import { App } from "./App";
6+
import { messages } from "./lib/i18n";
7+
8+
vi.mock("./components/GraphTab", () => ({ GraphTab: () => null }));
9+
vi.mock("./components/StatsTab", () => ({ StatsTab: () => null }));
10+
vi.mock("./components/ControlTab", () => ({ ControlTab: () => null }));
11+
vi.mock("./lib/i18n", async (importOriginal) => {
12+
const actual = await importOriginal<typeof import("./lib/i18n")>();
13+
return { ...actual, useUiMessages: () => messages.en };
14+
});
15+
16+
describe("App", () => {
17+
afterEach(() => {
18+
cleanup();
19+
vi.unstubAllGlobals();
20+
window.history.replaceState(null, "", "/");
21+
});
22+
23+
it("shows the serving binary version", async () => {
24+
vi.stubGlobal("fetch", vi.fn(async () =>
25+
new Response(JSON.stringify({ lang: "en", version: "0.10.8" }), {
26+
status: 200,
27+
headers: { "Content-Type": "application/json" },
28+
}),
29+
));
30+
31+
render(<App />);
32+
33+
expect(await screen.findByText("v0.10.8")).toBeVisible();
34+
});
35+
36+
it("hides the version when the config has no string version", async () => {
37+
const fetchMock = vi.fn(async () =>
38+
new Response(JSON.stringify({ lang: "en", version: 108 }), { status: 200 }),
39+
);
40+
vi.stubGlobal("fetch", fetchMock);
41+
42+
render(<App />);
43+
44+
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith("/api/ui-config"));
45+
expect(screen.queryByTitle("Server version")).not.toBeInTheDocument();
46+
});
47+
48+
it("hides the version when the config request fails", async () => {
49+
const fetchMock = vi.fn(async () => {
50+
throw new Error("offline");
51+
});
52+
vi.stubGlobal("fetch", fetchMock);
53+
54+
render(<App />);
55+
56+
await waitFor(() => expect(fetchMock).toHaveBeenCalledWith("/api/ui-config"));
57+
expect(screen.queryByTitle("Server version")).not.toBeInTheDocument();
58+
});
59+
});

‎graph-ui/src/App.tsx‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,24 @@ function routeUrl(tab: TabId, project: string | null): string {
3333
export function App() {
3434
const t = useUiMessages();
3535
const [route, setRoute] = useState<RouteState>(readRoute);
36+
const [version, setVersion] = useState<string | null>(null);
3637
const { tab: activeTab, project: selectedProject } = route;
3738

39+
useEffect(() => {
40+
let cancelled = false;
41+
void fetch("/api/ui-config")
42+
.then((response) => (response.ok ? response.json() : null))
43+
.then((config) => {
44+
if (!cancelled && typeof config?.version === "string" && config.version) {
45+
setVersion(config.version);
46+
}
47+
})
48+
.catch(() => {});
49+
return () => {
50+
cancelled = true;
51+
};
52+
}, []);
53+
3854
/* Normalize the URL on first load so it always carries the current route. */
3955
useEffect(() => {
4056
const initial = readRoute();
@@ -73,6 +89,14 @@ export function App() {
7389
<span className="text-[13px] font-semibold text-foreground/90 tracking-tight">
7490
Codebase Memory
7591
</span>
92+
{version && (
93+
<span
94+
className="translate-y-px text-[10px] font-mono text-foreground/30"
95+
title="Server version"
96+
>
97+
{version.startsWith("v") ? version : `v${version}`}
98+
</span>
99+
)}
76100
</div>
77101

78102
{/* Tabs inline in header */}

0 commit comments

Comments
 (0)