OpenCode reads either opencode.json or opencode.jsonc. We always wrote the
.json name, so a user whose real config is opencode.jsonc got a SECOND config
file that OpenCode ignores: the MCP server never appeared, while the installer
reported success. Silent, and indistinguishable from "the tool is broken".
The install target is now whichever file already exists, .jsonc first since it
is the one we used to miss. With neither present, .json is created exactly as
before, so fresh installs are unchanged. Other clients in this file (pochi,
kilo) already resolve .jsonc paths — OpenCode was the gap, not the exception.
Reported by @iandol in discussion #1560, who noticed their config was .jsonc
while the installer named .json. Verified by revert: the test fails without
the fix and passes with it.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The v0.10.3 release failed its `verify` gate:
--- Auditing: scan-...--manifest.json ---
BLOCKED: Unauthorized URL in binary: https://github.com/DeusData
BLOCKED: Unauthorized URL in binary: https://deusdata.github.io/...
=== BINARY STRING AUDIT FAILED ===
The blocked URLs are OURS — the .mcpb bundle manifest's own homepage and
documentation fields.
security-strings.sh already knows that URL and dangerous-command auditing are
"designed for compiled artifacts" and skips them for text, keeping the
credential and base64 audits that are universally meaningful. Its detection
just had no case for JSON: `file` reports "JSON data", which matched none of
the shell-script/ASCII-text patterns, so a manifest was audited as if it were a
compiled binary. An MCP manifest is exactly the shape that misfires — it
carries project URLs by design, and a `command` field by specification.
This is the first release to ship .mcpb bundles, which is why it surfaced now:
it is the first time a manifest reached the scanned object set. The bundles
were noted as end-to-end unproven until a stable release; this is that proof
doing its job.
Two changes:
- Classify structured-text artifacts by extension FIRST (.json/.yaml/.toml/
.md/...), then by `file` type with JSON/XML added. The extension check also
covers hosts without file(1), where the existing `command -v` guard silently
left every artifact classified as binary.
- Allow-list our own org root and documentation site, so a compiled binary
embedding the docs link (the graph UI does) passes on its own merits rather
than relying on the text exemption.
Tests: the manifest case, plus a control proving the exemption is narrow — the
credential audit still runs on structured text. The existing negative control
(an unauthorized URL in a real binary is still BLOCKED) is unchanged and still
passes, so the binary audit is not weakened.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Codex sanitizes the environment of stdio MCP subprocesses and passes through
only the names listed in `env_vars`. Our generated block had `command` and
`args` and nothing else, so a user with a custom CBM_CACHE_DIR got a
Codex-spawned server reading the DEFAULT cache while their account daemon used
the configured one. The two disagree during initialization, the connection
closes, and Codex exposes no cbm tools at all — a total failure that looks like
a broken MCP server rather than a missing environment variable.
The name is listed unconditionally rather than only when the variable happens
to be set during install: `env_vars` names variables to forward IF PRESENT, so
listing it costs nothing when unset, and it keeps working for someone who sets
CBM_CACHE_DIR after installing — a case install-time detection would silently
miss, producing exactly this bug again with no way to tell.
Reported by @anton-matosov, who verified the corrected block end-to-end before
filing.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The installed SKILL.md carried an unquoted `description` whose value contains
"Triggers on: ". In YAML a colon-space inside an unquoted scalar is a
nested-mapping indicator, so strict readers — js-yaml's `load`, the frontmatter
parser in `npx skills` — reject the whole document. The skill does not load
wrongly; it does not load at all, and nothing in the install output says why.
Quoting the scalar fixes it without touching a word of the text.
The test checks the RULE rather than this string: for every skill, any
frontmatter value containing ": " must be quoted. Pinning the current wording
would let the next skill someone adds reintroduce exactly this bug. Verified by
reverting the fix — the test goes red, and green again with it restored.
Reported by @xezzon.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Two contributors independently sent the same fix for #1550 while ours was
already in CI (#1553 by @musichen, who also reported the bug, and #1564 by
@henri-edh). Rather than close both as duplicates, this takes the part of
theirs that ours was missing.
@henri-edh's comment names the actual mechanism: Pi loads an extension via
jiti.import(path, { default: true }) and rejects anything that is not a
function, so a named-only export hands the loader the module namespace object
and the install fails with "Extension does not export a valid factory
function". Ours said "Pi calls the default export" — true, but it does not tell
the next reader why a named export cannot simply be added alongside, which is
exactly the change someone would try. Their wording is now ours.
The assertion is tightened the same way, from `export default function` to
`export default function (pi)`, so a default export with the wrong shape cannot
satisfy it.
Co-Authored-By: musichen <musichen@users.noreply.github.com>
Co-Authored-By: henri-edh <henri-edh@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The reporter came back with the output that named this:
exact executable identity could not be verified (cache-private) -
/Users/.../.cache/codebase-memory-mcp: ancestry component validation
failed (errno 2)
Two separate defects, and the second is why nobody could see the first.
**The refusal.** The daemon's private-directory ancestry walk rejected any
group-write bit on any ancestor — `(mode & 0022) == 0`. That is the identical
rule #1535 removed on the activation side, in the sibling gate the consolidated
strictness decision covers but that never got changed. A group-writable ~ or
~/.cache is ordinary: WSL2 ships 0775, so do several distro skeletons and any
site with a shared primary group. So the daemon refused to run, permanently,
with nothing the reader could close or restart.
World-writable is still refused (any local user could swap a path component),
and the private directory itself is still chmod'd to 0700 and verified after
the walk — which is what makes admitting the ancestor safe: the thing that
actually holds data stays owner-private either way. Group-writable ancestors
are logged, so the weaker check is never silent. Both directions are tested;
a fix that admitted everything would "fix" this by deleting the protection.
**The invented errno.** When that check failed, the branch set no detail, so
the caller fell back to printing `errno` — which nothing on the path sets. The
reporter was handed ENOENT for a permission refusal and went looking for a
missing file that was there all along; it cost a round trip to find out the
number meant nothing. The branch now names the component and the rule.
Reported by @listepo, who ran the diagnostics that turned this from a guess
into a one-line root cause.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The local Windows leg caught this within hours of merging #1532: on a machine
that is not domain-joined, USERDOMAIN is the literal string WORKGROUP, and
`WORKGROUP\test` is not a resolvable principal —
WARN: build-dir DACL stamp (pre-wave) failed (user=WORKGROUP\test ...):
WORKGROUP\test: No mapping between account names and security IDs was done.
so the grant fails outright and the tree keeps its inherited
`Authenticated Users:(M)`. The guard then correctly reports that cross-account
mutation is still possible, and the whole leg stops. Since most Windows
machines are not domain-joined, that is the common case, not the exotic one.
#1532 was fixing a real bug in the other direction (a bare name resolves
against the machine first, so a host named like its user grants to an empty
principal). Both failures are name RESOLUTION failures, so this stops resolving
names: the account is identified by SID, which is unambiguous everywhere. The
SYSTEM and Administrators grants in these same commands have always used that
form — the current user was the odd one out. Name lookup survives only as a
fallback where PowerShell is unavailable.
Applied to all three scripts carrying the stamp (run-tests-parallel, soak-test,
memlab), since they share the failure and would otherwise drift apart.
Co-Authored-By: Kiborgik <Kiborgik@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
`test-msan` failed the two backpressure cases while every other lane was green,
and the same suite passes under MSan locally (12/12). The wait that fired is a
LIVENESS BACKSTOP, not a race budget: the daemon child either announces on its
ready pipe or it does not, and the test asserts the announcement itself, never a
timing window.
A backstop only does its job if it never fires on a legitimately slow start.
30s was already the second value here — the first was below the daemon's own
12s cohort-claim budget and SIGKILLed healthy startups on oversubscribed
runners. Under MemorySanitizer, with instrumented libc++ and origin tracking,
startup runs several times slower than that same budget it has to clear, so 30s
stopped being a backstop and started being a verdict.
Sanitized builds now get 180s, matching the existing sanitizer-aware pattern in
test_cs_lsp_bench.c. This costs nothing when the daemon is healthy: a passing
run returns the moment the byte arrives, whatever the ceiling. Nothing about the
asserted state changed, and the non-sanitized bound is untouched.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A reporter ran `uninstall`, then could not install again: every attempt said
"active CBM sessions and operations could not be stopped safely". A reboot
proved no such session existed, and the remedy line v0.10.2 added made it worse
— it told them to run `codebase-memory-mcp daemon status`, a binary they had
just uninstalled.
Both halves were wrong for their case:
The guard funneled two unrelated outcomes into one message. A cohort that is
BUSY means real sessions hold it and closing something is the fix. A reservation
that FAILED — lock I/O, leftover coordination state, permissions — means nothing
is running at all, and telling that reader to close sessions sends them after
processes that do not exist. Same misattribution class as #1416 and #1535: a
non-session failure wearing a session costume. They are now separate messages,
and the failure one says plainly that nothing needs to be closed.
The remedy also has to survive the situation it prints in. Install, and any
retry after uninstall, runs with no cbm on PATH; advice that starts with
`codebase-memory-mcp ...` is unusable exactly when it is needed. The busy
message now leads with the actionable step (close the editor/agent holding it)
and mentions the command only as an aid where a binary still exists; the
failure message asks for a cache listing, which needs no cbm at all.
A recorded transaction refusal still outranks both generic messages, so the
#1416 attribution is unchanged.
This does NOT yet explain why their reservation fails with nothing running —
that needs their environment, which is being requested on the issue. What it
does is stop sending them after the wrong thing while we find out.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
`posix_spawnp` and `fork` both fail with EAGAIN when the process table or a
per-user limit is momentarily full. Both paths treated that as a hard failure,
so a burst of process creation turned into CBM_PROC_SPAWN_FAILED — a git probe,
an LSP server, or an index worker refusing to run for a reason the user can
neither see nor act on, on a machine that is merely busy.
This is the production half of a flake we have been re-running for two days:
`subprocess_run_clean` failed spawning `/bin/sh -c "exit 0"` on macOS runners in
three separate CI runs, always under a loaded release matrix, always green on a
quiet rerun. Chasing it as a test race would have been chasing the symptom — the
test is right, and a user on a loaded laptop hits the same edge.
EAGAIN/ENOMEM now retry three times over ~30ms; everything else keeps failing
immediately, and an exhausted system still fails after the backoff rather than
spinning. exec-class errors (ENOENT/EACCES/ENOEXEC/...) are untouched, so the
documented "missing binary exits 127, spawn_failed means the mechanism failed"
contract still holds. The fork fallback gets the same treatment: it fails for
the same reason under the same pressure, and the fallback must not be less
robust than the primary path.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
`trace_path(include_evidence:true)` added `strategy` and `confidence` per hop in
the default tree output, and returned `cols: ["name","hop"]` under
format:"json" — no error, no warning, just two fields missing from the response
shape the schema and `--help` both promise.
include_evidence was only ever implemented in bfs_to_tree_table; bfs_to_tree_json
never took the flag. So the callers most likely to want the evidence — the ones
asking for structured output to consume programmatically — were exactly the ones
who silently got none of it. Reported by @asemraza.
The json path now emits both columns with the same semantics as the tree path,
including the placeholder case: the root hop has no inbound edge and non-CALLS
edges record no strategy, so those rows carry nulls rather than dropping fields
and leaving callers with ragged rows. Tree says "-", json says null — the same
promise in the form each consumer can test.
The existing evidence test covered only the tree path, which is why this went
unnoticed; it now asserts both formats.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
#1545 fixed the defect that let concurrent instances quarantine each other's
healthy databases, but shipped without tests — and this is a data-loss path, so
it gets coverage before it ships.
Three cases, one per verdict, chosen so a regression in either direction fails:
healthy -> OK (the baseline the other two are measured against)
bad root_path-> CORRUPT (a verdict that never says CORRUPT would protect
broken databases instead of users)
no handle -> TRANSIENT (a file we could not open tells us NOTHING about
its contents; answering CORRUPT there is how a
database nobody had read got renamed and rebuilt)
Co-Authored-By: LynxBay <LynxBay@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Two regressions we shipped ourselves, both with the same shape: a change that
was correct in the abstract broke a concrete thing users had working.
**`update --ui` became a hard error (#1544).** v0.10.2 removed the ui/standard
chooser and deleted the flags that drove it, so a command in scripts, aliases
and muscle memory started failing with "unknown update option" where v0.10.1
had printed the install-script handoff. Retiring a CHOICE is fine; retiring the
WORDS is a break users get nothing for. Both flags are accepted again, do
nothing, and print one note explaining that every build now includes the UI.
A genuinely unknown flag is still rejected — the point is compatibility, not
silence. Reported by @ehendrix23.
**The generated Pi extension could not load (#1550).** Since #1392 the
extension has been generated from the tool registry, emitting
`export function register(pi)`. Pi loads an extension by calling its DEFAULT
export as a factory, so the file failed to load — and a Pi extension that fails
to load takes every `pi` command with it, `pi doctor` included, not just cbm's
tools. It now default-exports the factory. Reported by @musichen.
The existing generator test passed throughout: it asserted WHICH tools the file
lists, never that the file Pi loads is loadable. The new test pins the export
shape, which is the property that actually matters to the consumer.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
core.filemode=false hid the +x here, and release.yml invokes the
registry-entries generator directly — a 644 mode would fail the publish
job the first time it runs.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Every release now carries .mcpb one-click-install bundles alongside the
archives, and the MCP Registry entry lists them with per-file sha256:
- package-release.sh (canonical) builds codebase-memory-mcp-<target>.mcpb
for darwin/windows and the STATIC linux builds — manifest.json + the same
staged (stripped, gated) binary + LICENSE + THIRD_PARTY_NOTICES.md. The
glibc-dynamic linux targets stay archive-only: a dynamic binary defeats
the one-click promise.
- _build.yml / release-draft: bundles flow through provenance attestation,
checksums.txt, cosign signing and the release asset list; checksums.txt
is also preserved as a same-run artifact for the registry job.
- verify: the canonical scan matrix grows to 14 containers; MCPB manifests
are validated (parse, binary server, entry_point member, command binds
the entry point). Bundle binaries dedupe to the archive scan objects, so
the VT gate gains only the three distinct manifest.json files.
- publish-mcp-registry: gen-mcpb-registry-entries.sh appends one mcpb
package entry per bundle (release-asset URL + fileSha256 from the
attested checksums) to server.json before mcp-publisher runs.
Idempotent; a checksums file without bundles is a hard failure.
- contracts: Step 0o pins the bundle shape at its producer on every leg,
Step 0p pins the registry entries against the live server.json, and the
extractor contract covers the 14-container matrix incl. broken-manifest
fail-closed cases. The linux test image gains zip for the packager.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The quarantine path in resolve_store_internal() used the shallow
cbm_store_check_integrity() to decide whether to rename a project DB
to .corrupt and rebuild it. That check has two flaws that together
cause healthy databases to be destroyed under concurrent access:
1. It treats ANY sqlite3_prepare_v2 failure as corruption. Under
concurrent MCP instances (#1206), a writer lock held by another
instance makes the schema lookup return SQLITE_BUSY/LOCKED — a
transient condition, not damage. The losing instance quarantines
a perfectly healthy DB and rebuilds a sparse replacement.
2. It never runs PRAGMA quick_check, so it cannot catch real
page-level corruption either. A DB with a torn node/edge btree
but an intact projects table passes the check and keeps being
served (#1037) — the inverse failure.
This change introduces cbm_store_check_integrity_verdict(), which
returns a tri-state (OK / CORRUPT / TRANSIENT) and runs the shallow
check plus PRAGMA quick_check, classifying BUSY/LOCKED/IO-lock codes
as TRANSIENT. The quarantine decision in mcp.c now quarantines ONLY
on a confirmed CORRUPT verdict; TRANSIENT releases the mutation lease
and retries on the next resolve, never touching the DB file.
The existing cbm_store_check_integrity() and _deep() are unchanged,
so all other call sites (hot opens, artifact import, maintenance)
keep their current behavior.
Verified: builds clean with -Wall -Wextra -Werror; cli list_projects
and index_status run correctly against the patched binary.
Signed-off-by: LynxBay <187708458+LynxBay@users.noreply.github.com>
Five field reports in the 24 hours after v0.10.0 all pointed at the same thing:
gates that were right in principle refused real, ordinary setups, and then
failed to say why. Per the consolidated strictness decision, each gate keeps the
protection that matters and drops the part that was refusing legitimate users —
and every refusal now names what it refused and how to proceed.
**Daemon image gate: npx and every ephemeral install path (#1539, #1383).**
The admission check treated "the peer's image hashes differently" and "the
peer's image cannot be examined at all" as one failure. The second is what
`npx codebase-memory-mcp` always produces (ephemeral cache path,
unfingerprintable), so every npx-invoked client was rejected — and, because the
client never reported it, agents saw a transport that closed mid-handshake with
zero bytes on stdout. Reported by @wassolles with the admission path already
read and the fix space mapped.
An unverifiable image is now admitted: the rendezvous HELLO immediately above it
has already proven semantic version, build fingerprint, and protocol/store/
feature ABI, and the image check was trading that real proof for an unavailable
one. It logs daemon.client_image_unverifiable_admitted so the weaker check is
never invisible. A fingerprint MISMATCH — the tamper case the gate exists for —
still rejects hard. Separate test seams keep the two modes testable apart.
**Client bootstrap failures are no longer silent (#1539).**
An MCP client that cannot reach the daemon now emits a JSON-RPC error on stdout
naming the reason, plus the same text on stderr. Previously the reason sat in
bootstrap_result.message and the process exited having written nothing at all.
**POSIX activation: group-writable ancestors (#1535, discussion #1526).**
activation_directory_secure required no group or other write bit on the install
directory AND every ancestor. WSL2 ships ~ and ~/.local at 0775, as do several
distro skeletons and any site using a shared primary group, so install.sh failed
for a large fraction of Linux users — reporting a policy refusal as "activation
transaction I/O failed", which sent reporters after disk errors and filesystem
types. Root-caused by @AmirF194 in a clean ubuntu container; @shochdoerfer and
@iandol confirmed independently.
World-writable ancestors are still refused (any local user could swap a path
component mid-transaction). Group-writable ancestors are now warned about and
admitted. The LEAF directory stays strictly owner-private — that is where the
binary is published, and group write there would let another account replace the
executable between validation and exec. Refusals now name the directory, its
mode, and which rule refused.
**The obsolete ui/standard chooser (#1538, from discussion #1526).**
v0.10.0 consolidated to one archive per platform with the UI always embedded,
but `update` still offered a variant choice: "ui" could only 404, and "standard"
quietly WAS the UI build. Reported by @iandol upgrading 0.9.0 -> 0.10.0. The
chooser, its --standard/--ui flags, and the ui- URL plumbing are removed, along
with the CBM_VARIANT=ui remnant in the npm installer.
Already-released 0.9.x binaries cannot be fixed retroactively, so the release
workflow now publishes byte-identical ui-*-named alias assets — their updaters
work again with no user action. The aliases are uploaded AFTER the VirusTotal
gate: they are the same bytes as archives it already cleared, and uploading them
earlier would duplicate every object in the scan set and the provenance manifest.
**macOS install noise and attribution (#1537).**
install.sh silenced the "No such xattr: com.apple.quarantine" line, which is
what happens when a curl-downloaded archive carries no quarantine attribute —
harmless, and it became the title of a bug report about an unrelated failure.
The session-stop refusal now points at `daemon status` to list the client
processes actually holding the daemon, instead of asserting sessions exist and
leaving the reader to guess. Reported by @listepo.
**Riders.** hatchling is pinned in pkg/pypi (an unpinned backend resolved fresh
inside `python -m build` is what emitted Metadata-Version 2.5 and broke the
v0.10.1 publish); SECURITY.md's supported-versions table moves to 0.10.x.
Tests: separate seams for unverifiable vs mismatched peer images with a test per
outcome; activation refusal must name directory + mode + rule; a group-writable
ancestor must stage successfully. The update tests drop the flag that no longer
exists. Verified against each reporter's environment shape.
**Open security alerts (all three, OSSF Scorecard).**
- HIGH, binary artifact: an 8.8 MB compiled Go ELF wrapper had been committed at
pkg/go/codebase-memory-mcp by accident. Removed, and both it and its .exe
sibling are gitignored so `go build` in that directory cannot repeat it.
- HIGH, GHSA-2v37-7h3g-55p8: nanoid < 3.3.17 loops forever when a custom
generator is called with size 0. It reaches us transitively (postcss -> vite),
so it is pinned through the existing graph-ui overrides block rather than
promoted to a direct dependency; the lockfile resolves 3.3.18.
- MEDIUM, unpinned pip command: the publish step installed build/twine by
version only, leaving the whole transitive graph resolved at run time.
pkg/pypi/requirements-publish.txt now hash-pins the complete toolchain (316
hashes), generated on a linux/amd64 python:3.12 image so the wheels match what
ubuntu-latest resolves, and the step runs pip with --require-hashes. Verified
by installing from it in that same image.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The Windows DACL stamps in run-tests-parallel.sh, soak-test.sh and
memlab.sh pass the current user to icacls by bare name. Git Bash resolves
whoami to coreutils, which prints an unqualified name, and icacls resolves
an unqualified name against the machine before the user. On a host whose
name equals the user's (COMPUTERNAME=BUILD with user build) the grant
lands on an empty principal:
BUILD\:(OI)(CI)(F)
Combined with the /inheritance:r on the same invocation, the directory
ends up granting no usable account. run-tests-parallel.sh then cannot
write its own log directory and the wave dies with:
build/c/test-logs/results.txt: Permission denied
FAIL: test-runner --list-suites exited nonzero
Prefix the name with USERDOMAIN when it is set, which makes the account
unambiguous, and fall back to the bare name otherwise. SYSTEM and
Administrators in the same invocations already use SID form and are
unaffected.
Signed-off-by: PETRO YAKOVYSHYN <petroyakovyshyn@gmail.com>
win_security_init built its owner-only ACL with AddAccessAllowedAce, which
has no AceFlags parameter and can only emit an ACE with flags set to zero.
Applied to a directory together with PROTECTED_DACL_SECURITY_INFORMATION
that yields D:PAI(A;;FA;;;<user>): the protection severs the inherited ACEs
while the new ACE propagates nothing, so every child created afterwards is
born with an empty DACL and is unreadable even by its owner.
win_runtime_directory_secure is also a repair path. It continues on
ERROR_ALREADY_EXISTS and re-stamps the existing directory, so a tree
restored by hand with icacls is destroyed again on the next daemon or
client start.
Add a second ACL for containers, built with AddAccessAllowedAceEx and
CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE, and use it at the three
directory sites. Files and kernel objects are leaves and keep the flagless
ACL.
The container ACE carries FILE_ALL_ACCESS rather than GENERIC_ALL. Windows
splits an inheritable generic-rights ACE into an effective mapped ACE plus
an INHERIT_ONLY one holding the generic bits, and the owner-only DACL
validators require exactly one ACE, so the generic form fails every lock
directory with "secure CLI coordination could not be created
(project-locks)".
Fixes#1351
Signed-off-by: PETRO YAKOVYSHYN <petroyakovyshyn@gmail.com>
The v0.10.1 publish failed twice, for two stacked defects:
1. `python -m build` resolves the UNPINNED hatchling backend fresh inside its
isolated build env, and current hatchling emits Metadata-Version 2.5 —
which the pinned twine==6.2.0 rejects as "'2.5' is not a valid metadata
version". Deterministic, and a time bomb: v0.10.0 published cleanly days
ago on the same pins. Verified locally on identical artifacts: twine 6.2.0
rejects, twine 7.0.0 passes. The pin moves to 7.0.0 and `twine check`
now runs at build time so a metadata regression fails BEFORE upload.
2. The job was not idempotent, breaking its own design comment ("if publish
fails, the release stays in draft so we can re-run"). Attempt 1 published
npm 0.10.1 and then died at twine; the re-run 403'd on its own success
("cannot publish over the previously published versions") and the release
wedged in draft. npm publish now skips when the exact version already
exists on the registry, and twine uploads with --skip-existing — both
registries treat immutable prior success as done, not as a collision.
Workflow-only diff. Unblocks re-dispatching the wedged v0.10.1 release.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The detector's 100x bound blocked the v0.10.1 release twice on macos-15-intel
with 101.1x — and control runs on a quiet arm64 host measure the SAME curve
as 57-68x, deterministically. The resolve path has never been linear for this
fixture shape: per-function cost grows 0.5ms -> 1.7ms from 100 to 2000
functions, the audited short-name-lookup/no-negative-memo superlinearity now
tracked as #1527. A bound calibrated as "linear plus generous overhead" on a
curve that was never linear measures host constants, not complexity.
The bound moves to 200x — mid-way in log space between the known ~60-100x
curve and clear quadratic near 400x — so a real quadratic regression still
fails by 2x margin while known behavior stops producing release-blocking
false alarms on slower hosts. The measured table is pinned in the test
comment; when #1527 fixes the curve, the bound comes back down to ~40x.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Three regressions shipped in 0.10.0 share one failure shape: an empty result
with a success status, indistinguishable from "nothing found" for the LLM
clients that are cbm's primary consumers.
1) structuredContent {} on the whole tree-format surface (#1522 bug 1).
#1488 replaced the duplicated payload with an EMPTY structuredContent
object while every tool still declared a blanket permissive outputSchema.
Spec-honoring clients (Claude Code among them) treat structuredContent as
THE result when a schema is declared, so search_graph, trace_path,
query_graph, get_architecture, search_code, and detect_changes all rendered
as literally "{}" on their DEFAULT format, on every platform. The corrected
contract: no tool declares an outputSchema (tool output is
format-parameter-polymorphic — no static schema is truthful), JSON-object
payloads keep their parsed structuredContent, errors keep
structuredContent.error, and text-shaped payloads carry NO structuredContent
key at all — which also preserves #1375's no-duplication win.
2) Frontend queue overflow killed the session (found by the #1522 sweep).
Any 7+ requests pipelined in one stdin burst — an agent issuing parallel
tool calls does exactly this — overflowed the 8-frame frontend queue, which
failed the whole session: rc=1 with ZERO bytes of output, every buffered
response lost. A full queue is now backpressure: the stdin reader blocks
until the worker drains (bounded by the same stop/fail flags every teardown
path already sets); only a single frame larger than the entire 12 MiB byte
budget — which could never be admitted — remains a hard failure.
3) config get printed "" with exit 0 for every unset and every unknown key
(#1522 bug 2). list printed stored-or-DEFAULT while get printed
stored-or-EMPTY, and no subcommand validated key names, so a typo was
indistinguishable from a correctly-read setting. One config-key table now
drives help, list, get, set, and reset: get prints the stored value or the
key's real default (the same fallback the runtime readers use), and unknown
keys error with exit 1 on get, set, and reset alike.
Tests — each RED on the pre-fix tree and RED again on revert:
* test_mcp.c: text results carry no structuredContent key; tools/list
declares no outputSchema; the tool-table guard now binds all three
branches (absent / parsed-object / error) for every registered tool.
* test_daemon_frontend.c: the over-capacity contract flips from
"session fails" to "backpressure without loss" — the held first request
plus all 32 over-capacity frames are answered and the run closes cleanly.
* test_cli.c: the config command contract — defaults, round-trip, reset,
and unknown-key rejection on all three subcommands.
* smoke-test.sh: Phase 3z rewritten to the corrected structuredContent
contract, new Phase 3z1 (default-format replies usable in schema-honoring
clients, no outputSchema advertised), 3z2 (24 pipelined calls all
answered), 3z3 (config defaults + unknown-key rejection) — all asserted
against the SHIPPED artifact, where #1488's smoke phase previously pinned
the empty-object behavior as correct.
Verified end-to-end on the locally built production binary: all 15 tools
declare no schema; text tools return ABSENT structuredContent, object tools
populated, error envelopes intact; 7/24/64-deep pipelined bursts all answered
with rc=0; config get prints real defaults and exits 1 on unknown keys.
hook_augment (structuredContent.projects) and index_resilience
(structuredContent.status) consume object payloads and are unaffected.
Fixes#1522.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
check_index_coverage's inputSchema declared a top-level anyOf requiring
either 'paths' or 'scopes'. Anthropic (Claude), AWS Bedrock, Google
Vertex/Gemini, and Azure OpenAI all reject custom tool input schemas
containing a top-level oneOf/allOf/anyOf, so any MCP client backed by
one of these providers fails once this tool's schema reaches the model
(e.g. Claude: 'tools.N.custom.input_schema: ... anyOf is not supported
at the top level'), breaking every tool call in the session.
handle_check_index_coverage already enforces 'at least one of paths or
scopes' at runtime independently of the JSON Schema (path_count == 0
&& scope_count == 0 rejection), so dropping the schema-level anyOf does
not weaken validation. The requirement is now stated in the tool
description and in the paths/scopes property descriptions instead.
Fixes#1524
Signed-off-by: DaveMurray-AIGuys <264998796+DaveMurray-AIGuys@users.noreply.github.com>
test_py_lsp_bench.c:262 failed the v0.10.0 release run TWICE on genuine
attempts (ubuntu-24.04-arm 3/3), while the SAME sha passed that shard in the
morning's dry run. A wall-clock bound on a shared 4-vCPU runner is not a
property of the code, and this repo's testing doctrine says exactly that:
perf tests assert a ratio; a time bound may exist only as a liveness backstop.
This test's perf content already IS ratio-form (resolution ratio with a
sanitizer-aware floor). The 150/1500 ms budget was the anti-pattern half.
It becomes a 30 s liveness backstop so a catastrophic hang still fails the
suite rather than the 900 s shard wall clock, and absolute timings stay where
they belong: scripts/benchmark-*.sh, which record rather than gate.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Both Linux soak-quick legs in the dry run failed with:
FAIL: RSS 17592186044293MB > 200MB ceiling
17592186044293 MB is 2^44-123 -- a NEGATIVE ~123MB wrapped through size_t. The
diagnostics snapshot reported mi_process_info()'s current_rss verbatim, and on
Linux mimalloc never sets that field: it keeps mimalloc's internal
committed-page counter, which this project deliberately tunes low
(purge_decommits=1, purge_delay=0) and which can transiently go negative under
purge. macOS and Windows are untouched -- mimalloc reports real RSS there --
which is exactly the observed split: both Linux legs red, both macOS legs green.
mem.c already learned this lesson in full (its comment block documents the same
trap for cbm_mem_over_budget): cbm_mem_rss() is /proc-statm-primary on Linux and
mimalloc-correct elsewhere, and cbm_mem_peak_rss() reconciles peak >= current.
The diagnostics writer now uses both instead of the raw mi fields, so the soak
leak gates measure actual process RSS on every platform.
Verified: diagnostics suite passes; compiles and formats clean.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The first full dry run after #1508 failed in all three smoke-linux-portable
legs: _smoke.yml still expanded a variant matrix and extracted
codebase-memory-mcp-ui-<os>-<arch>.tar.gz — a name the build no longer
produces. PR CI never sees this job (pr.yml calls the smoke wrappers directly),
so the miss only surfaced in the dry-run/release path this workflow serves.
The matrix loses its variant dimension, all three legs extract the unsuffixed
archive, and the positional/SMOKE_VARIANT plumbing is replaced by
SMOKE_REQUIRE_UI=1: these legs smoke the SHIPPED artifact, so a binary serving
no embedded UI is a defect here, exactly like scripts/ci/smoke-artifact.sh.
Verified locally: venue-parity and smoke-fixture contracts pass; YAML parses.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Root cause of the Cyrillic guard failure, found via the finalize diagnostics:
seal_existing rc=-1. The primary store open normalizes the path through
cbm_path_for_file_api (UTF-8 -> wide -> UTF-8, the form SQLite's Windows VFS
needs), but four maintenance opens passed the raw UTF-8 path:
cbm_store_seal_existing_path_for_replace (the one this path hits)
cbm_store_prepare_path_for_replace
cbm_store_backup_path (both source and staging)
cbm_store_dump_to_file
So a generation was WRITTEN through the normalized path and then could not be
RE-OPENED through the raw one. On the second index into a cache the seal step
failed, finalize returned PERSIST_FAILED, and the user was told "Check repo_path
exists and contains source files" about a repo that had just indexed cleanly
(nodes=12 edges=21 in the same worker log).
On POSIX cbm_path_for_file_api is a no-op memcpy, which is why the full 3-OS
ladder and every non-Windows test passed -- the defect is Windows-only and only
manifests on a path the raw UTF-8 -> SQLite open mishandles. It surfaced on the
Cyrillic case first because the guard indexes the same repo TWICE (once for the
count, once on retry), making it the first case to re-open an existing
generation; the earlier ASCII/latin1 cases index into fresh caches.
All four now use cbm_path_for_file_api exactly as the primary open does. Also
upgrades dump_to_file from sqlite3_open to sqlite3_open_v2 with explicit flags.
The finalize.prepare_failed / rename_failed diagnostics from the previous commit
stay: a silent PERSIST_FAILED that blamed the repo cost a day of remote bisection
and must never be silent again.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The Cyrillic guard failure came down to this: the worker indexes the repo
perfectly (gbuf.dump nodes=12 edges=21, DB written, sealed, sidecars staged) and
then finalize fails somewhere between stage_sidecars and prepare_live -- and
every failure edge on that path returns PERSIST_FAILED without a word. The user
then reads "Pipeline failed. Check repo_path exists and contains source files"
about a repo that indexed flawlessly.
A silent failure that misattributes itself cost a full day of remote-only
bisection: five instrumentation commits in the TEST were needed to even locate
the failing function. That is a product defect independent of the original bug.
Every edge now logs before returning -- destination-not-regular, seal rc,
sidecar-cleanup errno (both branches), and the final rename with stage and
destination paths. The next guard run names the exact edge and value instead of
requiring a sixth probe.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The CLI probe reproduces the empty pipeline (rc=1, same one-line hint), so the
remaining record is the worker's own log -- which the supervisor unlinks on a
clean exit. CBM_PROFILE=1 flips that exact branch to retention
(index.supervisor.profile_log) with no product change, and the probe now prints
every retained log tail from its isolated cache.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>