From 8ef6038523fdfbca55bb69560447bbc0e49df14c Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Thu, 20 Aug 2026 18:43:36 -0400 Subject: [PATCH 1/5] fix(witan): route inject-context through the deployment for a remote target MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `witan hook context` handed cfg.graph_uri straight to OmnigraphClient without ever consulting load_remote_config(). A remote_url-only target has no server field, so graph_uri fell through to the local default — the same split-brain #261 fixed for `witan serve`, still live on the context-injection path. Every new agent session on a deployed target opened with context read off the laptop while its own tool calls wrote the deployment. Route the remote case through the same tool-calling proxy _srv() builds (workflow_project_list/task_ready/workflow_session_list), matching how session-checkpoint already reaches the deployment. Branch-linked task association and stale-repo-case detection have no remote tool equivalent yet, so they degrade to empty/false for a remote target rather than lie — recorded via --debug, per tk-witan-hook-context- reads-the-local-store-on-a-de-dfb2c9. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Chdha3LCrycxhHUhTq2teK --- mcp/servers/witan/tests/test_context.py | 33 +++---- mcp/servers/witan/witan/cli/hooks.py | 31 ++++++- mcp/servers/witan/witan/context.py | 112 +++++++++++++++++++++++- 3 files changed, 152 insertions(+), 24 deletions(-) diff --git a/mcp/servers/witan/tests/test_context.py b/mcp/servers/witan/tests/test_context.py index 5d6602bc..5306bbb4 100644 --- a/mcp/servers/witan/tests/test_context.py +++ b/mcp/servers/witan/tests/test_context.py @@ -794,16 +794,16 @@ def test_inject_context_cli_survives_an_unreachable_deployment( ): """A configured-but-down deployment must not reach an agent's prompt. - Worth pinning rather than assuming, because the shape here is not the one - the rest of the CLI has: this command never calls ``_srv()``, so it does not - go through ``RemoteMCPProxy`` at all — a remote-configured hook still reads - ``graph_uri`` directly. That means the unreachable-remote classification - added for every other command is not what protects this path; the blanket - guard inside ``ctx_module.inject_context`` is. Both a dead deployment and a - dead graph URI are set, so whichever one it does reach, stdout stays empty. + A `remote_url`-only target has no direct graph endpoint the CLI is + allowed to open, so this command routes through the same tool-calling + proxy `_srv()` builds for every other command (agent-kit#261's mechanism, + fixed here for this path too — see + tk-witan-hook-context-reads-the-local-store-on-a-de-dfb2c9). It must + degrade to an empty block rather than raise when the deployment cannot be + reached, exactly like `session-checkpoint` already does. """ from witan.cli import hooks - from witan.graph import OmnigraphClient + from witan_core.remote.proxy import RemoteMCPProxy cfg_file = tmp_path / "config.toml" cfg_file.write_text("") @@ -811,22 +811,17 @@ def test_inject_context_cli_survives_an_unreachable_deployment( monkeypatch.delenv("WITAN_TARGET", raising=False) monkeypatch.setenv("WITAN_REMOTE_URL", "https://witan.invalid/mcp") monkeypatch.setenv("WITAN_OIDC_ISSUER", "https://sso.invalid/realms/ol") - # An http(s) URI so the "local store missing" early return is skipped and - # the read is genuinely attempted. Raised rather than actually dialled: the - # real client rides out a restart with a connect-retry budget, which is - # right in production and minutes of nothing in a test. - monkeypatch.setenv("WITAN_MEMORY_URI", "https://omnigraph.invalid") attempted: list[str] = [] - def _refused(_self, _query_file, query_name, _params): - attempted.append(query_name) - raise RuntimeError("omnigraph: connection refused") + async def _refused(_self, name, _args, _kwargs): + attempted.append(name) + raise RuntimeError("witan.invalid: connection refused") - monkeypatch.setattr(OmnigraphClient, "read", _refused) + monkeypatch.setattr(RemoteMCPProxy, "_invoke", _refused) hooks.inject_context() # must not raise assert capsys.readouterr().out == "" - # Not vacuous: the hook got as far as a read before degrading. A cached - # block, or an early return, would leave this empty. + # Not vacuous: the hook got as far as a tool call before degrading. A + # cached block, or an early return, would leave this empty. assert attempted diff --git a/mcp/servers/witan/witan/cli/hooks.py b/mcp/servers/witan/witan/cli/hooks.py index d4c68878..561ec062 100644 --- a/mcp/servers/witan/witan/cli/hooks.py +++ b/mcp/servers/witan/witan/cli/hooks.py @@ -37,15 +37,38 @@ def inject_context(*, debug: bool = False) -> None: # document by design, so one stray character in a `[targets.*]` table takes # out context injection entirely — and also for a `WITAN_TARGET` naming a # target that isn't defined, which breaks the hook with a perfectly valid - # config file. SystemExit is not an Exception and `_srv()` raises it for a - # half-configured remote; letting either escape breaks the "never blocks" - # contract this command documents. Same guard as `session-checkpoint`. + # config file. SystemExit is not an Exception and `load_remote_config()` + # raises it for a half-configured remote; letting either escape breaks the + # "never blocks" contract this command documents. Same guard as + # `session-checkpoint`. try: - cfg = cfg_module.load() + remote = cfg_module.load_remote_config() except (Exception, SystemExit) as exc: # noqa: BLE001 — never fail the hook if debug: logger.debug("witan.hook.config_load_failed", error=str(exc), exc_info=True) return + + if remote is not None: + # A `remote_url`-only target has no direct graph endpoint to hand + # OmnigraphClient — that fallback is exactly #261's mechanism + # (tk-witan-hook-context-reads-the-local-store-on-a-de-dfb2c9), so a + # deployed target reads through the same tool-calling proxy `_srv()` + # would build, not `cfg_module.load()`'s local-store `graph_uri`. + from ._common import remote_proxy + + text = ctx_module.inject_context_remote( + remote_proxy(remote), remote.url, debug=debug + ) + if text: + print(text) + return + + try: + cfg = cfg_module.load() + except (Exception, SystemExit) as exc: # never fail the hook + if debug: + logger.debug("witan.hook.config_load_failed", error=str(exc), exc_info=True) + return graph_path = ( Path(cfg.graph_uri) if not cfg.graph_uri.startswith(("http://", "https://", "s3://")) diff --git a/mcp/servers/witan/witan/context.py b/mcp/servers/witan/witan/context.py index 8b8bc823..9dfc85f3 100644 --- a/mcp/servers/witan/witan/context.py +++ b/mcp/servers/witan/witan/context.py @@ -313,6 +313,38 @@ def inject_context( f"sessions_for={len(sessions_by_project)} project(s)", ) + return _render_context_block( + cache_key=graph_uri, + repo=repo, + branch=branch, + projects=projects, + sessions_by_project=sessions_by_project, + ready=ready, + open_branch_tasks=open_branch_tasks, + stale_repo_case=stale_repo_case, + debug=debug, + ) + + +def _render_context_block( + *, + cache_key: str, + repo: str | None, + branch: str | None, + projects: list[dict], + sessions_by_project: dict[str, list[dict]], + ready: list[dict], + open_branch_tasks: list[dict], + stale_repo_case: bool, + debug: bool, +) -> str: + """Render the markdown block both the local and remote data paths share. + + ``cache_key`` stands in for ``graph_uri`` in the output-cache key — for the + remote path (:func:`inject_context_remote`) that is the deployment's own + URL rather than a store path, so a laptop's local graph and a deployment + never collide on the same cache file (see ``_output_cache_file``). + """ lines: list[str] = [] if projects: @@ -402,7 +434,7 @@ def inject_context( # Cache the freshly rendered block (including an empty one) so the next # prompt in this window skips the graph reads entirely. - _write_output_cache(graph_uri, repo, branch, output) + _write_output_cache(cache_key, repo, branch, output) _dbg( debug, f"rendered {len(output)} chars" @@ -411,6 +443,84 @@ def inject_context( return output +def inject_context_remote(server, remote_url: str, debug: bool = False) -> str: + """Same rendered block as :func:`inject_context`, sourced from the + deployment's own MCP tool surface instead of a direct :class:`OmnigraphClient` + read. + + A ``remote_url``-only deployment target has no direct graph endpoint the + CLI is allowed to open — ``inject_context`` calling ``OmnigraphClient`` + unconditionally is exactly the bug this function fixes (agent-kit#261's + mechanism, still live on this path: see + tk-witan-hook-context-reads-the-local-store-on-a-de-dfb2c9). ``server`` is + a tool-calling proxy built the same way ``_srv()`` builds one for a remote + target (``witan.cli._common.remote_proxy``), so this makes exactly the + tool calls an agent's own session would make. + + Degrades relative to :func:`inject_context`: branch-linked task + association (the ``## In-Flight Branch`` section) and stale-repo-case + detection have no remote-tool equivalent yet and are always empty/False + here — recorded via ``--debug``, never silently. + """ + try: + repo, branch = _cached_repo_and_branch() + _dbg(debug, f"detected repo={repo!r} branch={branch!r} (remote)") + _dbg( + debug, f"remote_url={remote_url!r} output_cache_ttl={_output_cache_ttl()}s" + ) + + cached = _read_output_cache(remote_url, repo, branch) + if cached is not None: + _dbg(debug, f"served from output cache ({len(cached)} chars)") + return cached + + # Mirrors the local path's gating: no detected repo means no project + # list (workflow_project_list(repo="") would return every repo's + # active projects, not "none" — see its docstring), but ready tasks + # still fall through to the unscoped set either way (task_ready's own + # repo_module.detect(override="") resolves to "no repo", matching + # list_unscoped_tasks locally). limit=10000 matches the local path's + # list_unscoped_tasks cap so a busy graph's header count isn't + # silently truncated by task_ready's own default limit=20. + projects = ( + server.workflow_project_list(repo=repo, status="active") if repo else [] + ) + ready = server.task_ready(repo=(repo or ""), limit=10000) + _dbg(debug, f"remote reads OK: projects={len(projects)} ready={len(ready)}") + except Exception: # noqa: BLE001 + _dbg_exc(debug, "FAILED building remote context (returning empty block)") + return "" + + sessions_by_project: dict[str, list[dict]] = {} + for p in projects[:3]: + try: + sessions_by_project[p["slug"]] = server.workflow_session_list( + project_slug=p["slug"] + ) + except Exception: # noqa: BLE001 + _dbg_exc( + debug, f"sessions read failed for {p['slug']!r} (skipping resume lines)" + ) + + _dbg( + debug, + "remote mode: branch-task linkage and stale-repo-case detection " + "unavailable (no remote tool equivalent yet)", + ) + + return _render_context_block( + cache_key=remote_url, + repo=repo, + branch=branch, + projects=projects, + sessions_by_project=sessions_by_project, + ready=ready, + open_branch_tasks=[], + stale_repo_case=False, + debug=debug, + ) + + # A project sitting through this many sessions in the same phase without # advancing is a soft signal that it may be stuck — nudge, don't block. _STALE_SESSION_THRESHOLD = 4 From eadf6bf1007512eec4aded586ca9da1757fc9ce8 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Fri, 21 Aug 2026 09:10:59 -0400 Subject: [PATCH 2/5] test(witan): cover the successful remote inject-context path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The only remote-path test forced the proxy to fail — a regression in tool names/arguments, session aggregation, or rendering could ship with that test still green. Add a fake-proxy test asserting the exact workflow_project_list/task_ready/workflow_session_list calls and the rendered text, plus coverage for the no-repo call-gating and the cache key being the deployment URL. Addresses copilot-pull-request-reviewer feedback on #272. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Chdha3LCrycxhHUhTq2teK --- mcp/servers/witan/tests/test_context.py | 131 ++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/mcp/servers/witan/tests/test_context.py b/mcp/servers/witan/tests/test_context.py index 5306bbb4..9ff6c7d4 100644 --- a/mcp/servers/witan/tests/test_context.py +++ b/mcp/servers/witan/tests/test_context.py @@ -825,3 +825,134 @@ async def _refused(_self, name, _args, _kwargs): # Not vacuous: the hook got as far as a tool call before degrading. A # cached block, or an early return, would leave this empty. assert attempted + + +class _FakeRemoteServer: + """Records every call it receives and answers with fixed, known data. + + Stands in for the tool-calling proxy ``_srv()``/``remote_proxy()`` build — + ``inject_context_remote`` only ever calls attribute-style methods on it, the + same shape ``RemoteServerProxy.__getattr__`` returns, so a plain class with + matching method names is a faithful substitute without dialling out. + """ + + def __init__(self, projects, ready, sessions_by_project): + self.calls: list[tuple[str, dict]] = [] + self._projects = projects + self._ready = ready + self._sessions_by_project = sessions_by_project + + def workflow_project_list(self, **kwargs): + self.calls.append(("workflow_project_list", kwargs)) + return self._projects + + def task_ready(self, **kwargs): + self.calls.append(("task_ready", kwargs)) + return self._ready + + def workflow_session_list(self, **kwargs): + self.calls.append(("workflow_session_list", kwargs)) + return self._sessions_by_project.get(kwargs.get("project_slug"), []) + + +def test_inject_context_remote_reads_through_the_proxy(tmp_path, monkeypatch): + """The successful remote path, not just its failure mode. + + ``test_inject_context_cli_survives_an_unreachable_deployment`` above only + proves a dead proxy degrades to empty output — it never exercises a + proxy that actually answers, so a regression in the tool names/arguments, + session aggregation, or rendering could ship with that test still green + (Copilot review on agent-kit#272). This pins the exact calls made and the + resulting text. + """ + from witan import context as ctx_module + + monkeypatch.setenv("TMPDIR", str(tmp_path)) + monkeypatch.setenv("WITAN_CONTEXT_TTL", "0") + import tempfile + + monkeypatch.setattr(tempfile, "tempdir", None) + + repo = "https://github.com/test/ctx-remote" + monkeypatch.setenv("WITAN_REPO", repo) + + server = _FakeRemoteServer( + projects=[ + {"slug": "wp-remote", "title": "Remote Project", "phase": "implementation"} + ], + ready=[ + { + "slug": "tk-remote", + "title": "Remote Task", + "priority": "p1", + "status": "open", + } + ], + sessions_by_project={}, + ) + + text = ctx_module.inject_context_remote(server, "https://witan.example.org/mcp") + + assert ("workflow_project_list", {"repo": repo, "status": "active"}) in server.calls + assert ("task_ready", {"repo": repo, "limit": 10000}) in server.calls + assert ( + "workflow_session_list", + {"project_slug": "wp-remote"}, + ) in server.calls + + assert "## Active Workflow Projects" in text + assert "Remote Project" in text + assert "wp-remote" in text + assert "## Ready Tasks" in text + assert "Remote Task" in text + # No remote-tool equivalent exists yet for either — must be absent, not + # just incidentally empty (see inject_context_remote's docstring). + assert "## In-Flight Branch" not in text + assert "Unmigrated Repo Keys" not in text + + +def test_inject_context_remote_no_repo_skips_project_list(tmp_path, monkeypatch): + """Mirrors the local path's own gating: outside a detected repo, no + project list is fetched at all (calling ``workflow_project_list(repo="")`` + would return every repo's active projects, not "none" — see its + docstring), but ready tasks still resolve to the unscoped set.""" + from witan import context as ctx_module + + monkeypatch.setenv("TMPDIR", str(tmp_path)) + monkeypatch.setenv("WITAN_CONTEXT_TTL", "0") + import tempfile + + monkeypatch.setattr(tempfile, "tempdir", None) + monkeypatch.setenv("WITAN_REPO", "") # explicitly disables repo detection + + server = _FakeRemoteServer(projects=[], ready=[], sessions_by_project={}) + + ctx_module.inject_context_remote(server, "https://witan.example.org/mcp") + + assert not any(name == "workflow_project_list" for name, _ in server.calls) + assert ("task_ready", {"repo": "", "limit": 10000}) in server.calls + + +def test_inject_context_remote_cache_key_is_the_deployment_url(tmp_path, monkeypatch): + """The cache key is the deployment URL, not a store path — two different + deployments must not collide on one cache entry, and a repeat call within + the TTL must not re-hit the proxy at all.""" + from witan import context as ctx_module + + monkeypatch.setenv("TMPDIR", str(tmp_path)) + import tempfile + + monkeypatch.setattr(tempfile, "tempdir", None) + monkeypatch.setenv("WITAN_REPO", "https://github.com/test/ctx-remote-cache") + + server = _FakeRemoteServer(projects=[], ready=[], sessions_by_project={}) + + ctx_module.inject_context_remote(server, "https://witan-a.example.org/mcp") + calls_after_first = len(server.calls) + ctx_module.inject_context_remote(server, "https://witan-a.example.org/mcp") + # Same deployment, second call: served from cache, no fresh proxy read. + assert len(server.calls) == calls_after_first + + ctx_module.inject_context_remote(server, "https://witan-b.example.org/mcp") + # A different deployment's URL must not read the first one's cache. + assert len(server.calls) > calls_after_first From 4c30b1fc03f8c50b756854de1b2052ad295861da Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Fri, 21 Aug 2026 10:01:57 -0400 Subject: [PATCH 3/5] fix(witan): route project/trace/session show through tools for remote targets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `witan project show`, `witan trace show`, and `witan session list` called `s.client.read(...)` directly, bypassing the MCP tool layer. That works in-process, where `s.client` is a real omnigraph client, but `RemoteServerProxy` has no `client` — `__getattr__` handed back a plain dispatch closure, and `.read(...)` on it raised `AttributeError: 'function' object has no attribute 'read'` against any deployed target. Routed through existing tools instead: `workflow_project_get_blockers`, `workflow_trace_get`, and a new `include_superseded` flag on `workflow_session_list` (for `session list`'s dedupe view, the one caller that wants superseded rows back). All three dispatch correctly against either target. Regression-tested end to end against a real `RemoteServerProxy` wired to an in-memory FastMCP server, not just the local guard. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015DWYCk5hnBjCiwXQPuGok1 --- mcp/servers/witan/tests/test_remote_proxy.py | 88 ++++++++++++++++++- mcp/servers/witan/witan/cli/local_dispatch.py | 31 ++++--- mcp/servers/witan/witan/cli/projects.py | 21 ++--- mcp/servers/witan/witan/cli/session.py | 4 +- mcp/servers/witan/witan/cli/traces.py | 15 +--- mcp/servers/witan/witan/server.py | 8 +- 6 files changed, 124 insertions(+), 43 deletions(-) diff --git a/mcp/servers/witan/tests/test_remote_proxy.py b/mcp/servers/witan/tests/test_remote_proxy.py index 2722bd69..c561ef77 100644 --- a/mcp/servers/witan/tests/test_remote_proxy.py +++ b/mcp/servers/witan/tests/test_remote_proxy.py @@ -731,4 +731,90 @@ def spy(name, args, kwargs): t = proxy.task_create(title="no sid", description="d", repo=REPO) proxy.task_claim(slug=t["slug"]) - assert "session_id" not in captured["task_claim"] + + +# ── CLI read commands that bypass the tool layer (agent-kit#270) ──────────── +# `witan project show`, `witan trace show` and `witan session list` used to +# call `s.client.read(...)` directly instead of going through a tool. +# `RemoteServerProxy` has no `client` — `__getattr__` handed back a plain +# dispatch closure, and `.read(...)` on that raised `AttributeError`. They now +# go through `workflow_project_get_blockers`/`workflow_trace_get`/ +# `workflow_session_list`, which dispatch correctly against either target. + + +@pytest.fixture +def _cli_against_proxy(proxy, monkeypatch): + """Point the CLI's `_srv()` at `proxy` and capture everything it prints.""" + from witan.cli import _common + + monkeypatch.setattr(_common, "_server", proxy) + printed: list[str] = [] + monkeypatch.setattr( + _common.console, + "print", + lambda *a, **kw: printed.append(str(a[0]) if a else ""), + ) + return printed + + +def test_project_show_works_against_a_remote_target(proxy, _cli_against_proxy): + from witan.cli.projects import _project_show + + printed = _cli_against_proxy + blocker = proxy.workflow_project_create(title="blocker", description="d") + blocked = proxy.workflow_project_create(title="blocked", description="d") + proxy.workflow_project_block(slug=blocker["slug"], blocks_slug=blocked["slug"]) + + sid = "11111111-1111-1111-1111-111111111111" + sess = proxy.workflow_session_start( + project_slug=blocked["slug"], session_id=sid, phase="discovery" + ) + proxy.workflow_session_end( + session_slug=sess["session_slug"], summary="did some work" + ) + + _project_show(blocked["slug"]) + + combined = "\n".join(printed) + assert blocker["slug"] in combined + assert sess["session_slug"] in combined + + +def test_trace_show_works_against_a_remote_target(proxy, _cli_against_proxy): + from witan.cli.traces import _trace_show + + printed = _cli_against_proxy + proj = proxy.workflow_project_create(title="ship it", description="d") + sid = "22222222-2222-2222-2222-222222222222" + sess = proxy.workflow_session_start( + project_slug=proj["slug"], session_id=sid, phase="implementation" + ) + proxy.workflow_session_end( + session_slug=sess["session_slug"], summary="did the work" + ) + proxy.workflow_project_complete( + slug=proj["slug"], + outcome="Delivered the feature end to end, verified in prod.", + ) + + _trace_show(proj["slug"]) + + combined = "\n".join(printed) + assert sess["session_slug"] in combined + assert "Delivered the feature end to end" in combined + + +def test_session_list_works_against_a_remote_target(proxy, _cli_against_proxy): + from witan.cli.session import session_list + + printed = _cli_against_proxy + proj = proxy.workflow_project_create(title="track sessions", description="d") + sid = "33333333-3333-3333-3333-333333333333" + sess = proxy.workflow_session_start( + project_slug=proj["slug"], session_id=sid, phase="discovery" + ) + proxy.workflow_session_end(session_slug=sess["session_slug"], summary="checkpoint") + + session_list(proj["slug"]) + + assert any(sess["session_slug"] in line for line in printed) diff --git a/mcp/servers/witan/witan/cli/local_dispatch.py b/mcp/servers/witan/witan/cli/local_dispatch.py index 54825911..446724d7 100644 --- a/mcp/servers/witan/witan/cli/local_dispatch.py +++ b/mcp/servers/witan/witan/cli/local_dispatch.py @@ -78,18 +78,23 @@ CLIENT_READ_ATTRS = frozenset({"read", "graph_uri"}) """What ``s.client.`` may reach on a fallback store. -Several read-only commands go around the tool surface and query the client -directly — ``witan session list`` (``session.py``), ``witan trace show`` -(``traces.py``) and ``witan project show`` (``projects.py``) all call -``s.client.read(...)``. Refusing the whole ``client`` attribute would break -three working read commands with a message about writes. +Some commands go around the tool surface and query the client directly rather +than calling a tool — ``witan migrate storage`` (``migrate.py``) prints the +store path via ``s.client.graph_uri``. ``witan session list``, ``witan trace +show`` and ``witan project show`` used to reach ``s.client.read(...)`` too, +which only works in-process (a deployed target has no client to reach past — +see agent-kit#270); they now go through +``workflow_session_list``/``workflow_trace_get``/``workflow_project_get_blockers`` +instead, which dispatch correctly either way. ``read`` stays in this allowlist +as a narrow escape hatch for the next command that needs it, not because +anything still calls it. Handing back the real client instead would be worse: it also carries ``change``/``change_many``/``load``, so the guard would be trivially side-steppable by the one code path that already bypasses the tool layer. -Hence a facade over exactly the two members those commands use — the query -call, and the store path ``witan migrate storage`` prints. Anything else on -the client refuses like any other write. +Hence a facade over exactly the two members a caller might reach for — the +query call, and the store path. Anything else on the client refuses like any +other write. """ @@ -184,11 +189,11 @@ class _LocalStoreGuard: A proxy rather than a check at each call site. There are ~50 dispatch points across the CLI package and they are not uniform — most go through - ``_fn(s.tool)``, ``witan migrate`` calls ``s.tool()`` directly, and three - read commands reach past both into ``s.client.read(...)`` — so a per-site - guard would be a list to keep in sync, and the one site somebody forgets is - indistinguishable from the bug. Attribute access is the single place every - one of them passes through. + ``_fn(s.tool)``, ``witan migrate`` calls ``s.tool()`` directly, and + ``witan migrate storage`` reaches past both into ``s.client.graph_uri`` — + so a per-site guard would be a list to keep in sync, and the one site + somebody forgets is indistinguishable from the bug. Attribute access is + the single place every one of them passes through. Holds the server module UNIMPORTED until an allowed read asks for it, because importing it is itself a write to the store this refuses to use. diff --git a/mcp/servers/witan/witan/cli/projects.py b/mcp/servers/witan/witan/cli/projects.py index 41f05eb8..078660de 100644 --- a/mcp/servers/witan/witan/cli/projects.py +++ b/mcp/servers/witan/witan/cli/projects.py @@ -94,24 +94,18 @@ def _project_show(slug: str) -> None: if p.get("github_pr"): console.print(f" pr: {p['github_pr']}") if p.get("blocked_by"): + blockers = { + b["slug"]: b for b in _fn(s.workflow_project_get_blockers)(slug=slug) + } for blocker in p["blocked_by"]: - rows = s.client.read( - "read.gq", "get_workflow_project_by_slug", {"slug": blocker} - ) - b = rows[0] if rows else None + b = blockers.get(blocker) st = b.get("status") if b else "missing" console.print(f" blocked by {blocker} [{_styled(st, _STATUS_STYLE)}]") if p.get("blocks"): console.print(f" blocks: {', '.join(p['blocks'])}") console.print(f"\n{p.get('description') or '(no description)'}\n") - sessions = [ - sess - for sess in s.client.read( - "read.gq", "list_sessions_by_project", {"project_slug": slug} - ) - if not sess.get("superseded_by") - ] + sessions = _fn(s.workflow_session_list)(project_slug=slug) console.print(f" sessions: {len(sessions)}") for sess in sessions: console.print( @@ -128,9 +122,8 @@ def _project_show(slug: str) -> None: ) if p.get("status") == "completed": - trace = s.client.read("read.gq", "get_trace", {"slug": f"wt-{slug}"}) - if trace: - tr = trace[0] + tr = _fn(s.workflow_trace_get)(slug=slug) + if tr: console.print( f"\n [blue]trace[/blue]: {tr.get('session_count')} sessions, " f"phases={tr.get('phases')}, duration={tr.get('duration')}h" diff --git a/mcp/servers/witan/witan/cli/session.py b/mcp/servers/witan/witan/cli/session.py index a8411e06..345b8a63 100644 --- a/mcp/servers/witan/witan/cli/session.py +++ b/mcp/servers/witan/witan/cli/session.py @@ -238,8 +238,8 @@ def session_list(project_slug: str) -> None: project_slug: The ``wp-`` slug whose sessions to list. """ s = _srv() - sessions = s.client.read( - "read.gq", "list_sessions_by_project", {"project_slug": project_slug} + sessions = _fn(s.workflow_session_list)( + project_slug=project_slug, include_superseded=True ) if not sessions: console.print(f"[dim]No sessions for {project_slug}.[/dim]") diff --git a/mcp/servers/witan/witan/cli/traces.py b/mcp/servers/witan/witan/cli/traces.py index 73baefea..e395ef21 100644 --- a/mcp/servers/witan/witan/cli/traces.py +++ b/mcp/servers/witan/witan/cli/traces.py @@ -76,11 +76,10 @@ def traces( def _trace_show(slug: str) -> None: """Show a trace's outcome, sessions, and mined lessons/patterns.""" s = _srv() - rows = s.client.read("read.gq", "get_trace", {"slug": slug}) - if not rows: + tr = _fn(s.workflow_trace_get)(slug=slug) + if not tr: console.print(f"[red]No trace {slug!r}.[/red]") return - tr = rows[0] console.print(f"[bold]{tr['slug']}[/bold] {tr.get('title', '')}") console.print( @@ -91,15 +90,7 @@ def _trace_show(slug: str) -> None: console.print(f"\n{tr.get('description') or '(no description)'}\n") console.print(f"[bold]Outcome[/bold]\n{tr.get('outcome') or '(none recorded)'}\n") - sessions = [ - sess - for sess in s.client.read( - "read.gq", - "list_sessions_by_project", - {"project_slug": tr.get("project_slug")}, - ) - if not sess.get("superseded_by") - ] + sessions = _fn(s.workflow_session_list)(project_slug=tr.get("project_slug")) if sessions: console.print("[bold]Sessions[/bold]") for sess in sessions: diff --git a/mcp/servers/witan/witan/server.py b/mcp/servers/witan/witan/server.py index 78296000..7234434e 100644 --- a/mcp/servers/witan/witan/server.py +++ b/mcp/servers/witan/witan/server.py @@ -4590,6 +4590,7 @@ def workflow_session_end( def workflow_session_list( project_slug: str | None = None, open_only: bool = False, + include_superseded: bool = False, ) -> list[dict]: """ List workflow sessions, newest last. @@ -4612,6 +4613,10 @@ def workflow_session_list( Only sessions with no ``ended_at``. Superseded sessions (deduped by ``witan migrate dedupe-sessions``) are always excluded — they are already skipped by every aggregate read and are not leaks. + include_superseded: + Keep superseded rows instead of dropping them. For ``witan session + list``, the one caller that wants to see what + ``migrate dedupe-sessions`` did rather than the leaked-session view. """ if project_slug: rows = client.read( @@ -4622,7 +4627,8 @@ def workflow_session_list( rows = [{**r, "project_slug": project_slug} for r in rows] else: rows = client.read("read.gq", "list_all_sessions", {}) - rows = [r for r in rows if not r.get("superseded_by")] + if not include_superseded: + rows = [r for r in rows if not r.get("superseded_by")] if open_only: rows = [r for r in rows if not r.get("ended_at")] return rows From ad7f5c189bcaf1d96c32338dda156e0c8839fa0d Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Fri, 21 Aug 2026 10:02:11 -0400 Subject: [PATCH 4/5] fix(witan-core): refresh the moved omnigraph edge pin again MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `edge` moved again the day after the last refresh, so `install_omnigraph` refused the checksum mismatch — correctly — and every witan-code CI job failed with no binary at all (agent-kit#272). Repins all three tiers to the 2026-08-21T00:11Z build (through upstream 62a9c3fe6b), verified by downloading each tarball, hashing it locally, and cross-checking the release's published `.sha256` in the same sitting. Version still reports 0.10.0 and internal-schema still 6, so no format migration. The six commits in between are all storage-layer typed-failure work (OmniError::Lance(String) -> OmniError::Storage(StorageFailure), RFC-0038). Checked the diff for the two things that would matter to witan — the _RETRYABLE/_NEEDS_REPAIR/_PRECONDITION_FAILED substrings and the "storage: " prose prefix _classify_cli_error keys on — and neither renamed; upstream's own new tests assert the same `storage: ` rendering. Also confirmed purely Rust-API-internal: upstream's own docs state the HTTP API and OpenAPI schema are unchanged, generic storage failures still map to 500, so nothing here is reachable from witan's CLI/HTTP integration either way. Verified locally: reinstalling from a clean state downloads and checksums the new binary, and the 10 previously-failing witan-code tests (tests/test_ingest.py) now pass. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015DWYCk5hnBjCiwXQPuGok1 --- docker/omnigraph-server.Dockerfile | 4 +- docker/witan.Dockerfile | 4 +- .../witan_core/omnigraph_install.py | 58 ++++++++----------- 3 files changed, 28 insertions(+), 38 deletions(-) diff --git a/docker/omnigraph-server.Dockerfile b/docker/omnigraph-server.Dockerfile index 54fb0f8f..ed3834ad 100644 --- a/docker/omnigraph-server.Dockerfile +++ b/docker/omnigraph-server.Dockerfile @@ -36,8 +36,8 @@ ARG OMNIGRAPH_VERSION=0.10.0 # Kept separate because on a moving tag the two differ — see # witan_core/omnigraph_install.py :: _OMNIGRAPH_RELEASE_TAG. ARG OMNIGRAPH_RELEASE_TAG=edge -ARG OMNIGRAPH_SHA256_X86_64=8ecabdbc3a11d60716f569b32de6710834ddcbba328c2342b77f5c529bb7bc4f -ARG OMNIGRAPH_SHA256_ARM64=aef871eeb070532947beee0f7644848552f59bbc8d4100a4bdad6d760acef647 +ARG OMNIGRAPH_SHA256_X86_64=68099e33941cc5c252f36d4c2a26f1dfff6b28e4eb627ba1f7bf098856d34349 +ARG OMNIGRAPH_SHA256_ARM64=d0e42176625584370a26c8a6ca43bc24447ea822483c46d971807475a5a5782a # ── Fetch + checksum-verify the release, extract both binaries ──────────────── FROM debian:trixie-slim@sha256:3a39a0592364683e6bab97937b72cad5a8fa6dcbbee90edb3bb48c7f8e94f258 AS fetch diff --git a/docker/witan.Dockerfile b/docker/witan.Dockerfile index d3d17b18..bebf1a0b 100644 --- a/docker/witan.Dockerfile +++ b/docker/witan.Dockerfile @@ -28,8 +28,8 @@ ARG OMNIGRAPH_VERSION=0.10.0 # Kept separate because on a moving tag the two differ — see # witan_core/omnigraph_install.py :: _OMNIGRAPH_RELEASE_TAG. ARG OMNIGRAPH_RELEASE_TAG=edge -ARG OMNIGRAPH_SHA256_X86_64=8ecabdbc3a11d60716f569b32de6710834ddcbba328c2342b77f5c529bb7bc4f -ARG OMNIGRAPH_SHA256_ARM64=aef871eeb070532947beee0f7644848552f59bbc8d4100a4bdad6d760acef647 +ARG OMNIGRAPH_SHA256_X86_64=68099e33941cc5c252f36d4c2a26f1dfff6b28e4eb627ba1f7bf098856d34349 +ARG OMNIGRAPH_SHA256_ARM64=d0e42176625584370a26c8a6ca43bc24447ea822483c46d971807475a5a5782a # Keep in lockstep with witan-council's version (mcp/servers/witan/pyproject.toml # [project].version / [tool.bumpversion]); it labels the built image. ARG WITAN_VERSION=0.8.0 diff --git a/packages/witan-core/witan_core/omnigraph_install.py b/packages/witan-core/witan_core/omnigraph_install.py index 552bd13a..af460396 100644 --- a/packages/witan-core/witan_core/omnigraph_install.py +++ b/packages/witan-core/witan_core/omnigraph_install.py @@ -122,38 +122,28 @@ #: and confirm it against the tarball you actually downloaded (`sha256sum`) in #: the same sitting — on a moving tag the two assets can be republished a #: minute apart, and a digest read across that gap describes neither build. -#: ★ THESE ARE THE `edge` BUILD OF 2026-08-20T17:18Z (through bee47cd465), -#: NOT v0.9.0's. Refreshed from the 2026-08-19T00:53Z triple (da466ba75b) -#: after CI failed the checksum check on 2026-08-20. Eight commits landed in -#: between. Five are RFC/docs (c03deee47f, 5cc8151f0b, 71dbe05250, 16aa8889e1, -#: bee47cd465) and one is upstream's own test inventory (0066d775d1). Two -#: needed reading, and both are the same vocabulary sweep: +#: ★ THESE ARE THE `edge` BUILD OF 2026-08-21T00:11Z (through 62a9c3fe6b), +#: NOT v0.9.0's. Refreshed from the 2026-08-20T17:18Z triple (bee47cd465) +#: after CI failed the checksum check on 2026-08-21 (agent-kit#272). Six +#: commits landed in between, all storage-layer typed-failure work +#: (docs/rfcs/0038-typed-storage-failures.md): `OmniError::Lance(String)` +#: became `OmniError::Storage(StorageFailure)`, closed over a shared +#: `omnigraph-storage` crate. Checked the diff for the two things that would +#: matter to witan — the `_RETRYABLE`/`_NEEDS_REPAIR`/`_PRECONDITION_FAILED` +#: substrings in omnigraph.py, and the `"storage: "` prose prefix witan's +#: classifier keys on — and found neither renamed: the refactor keeps +#: `STORAGE_MESSAGE_PREFIX = "storage: "` and its own new tests assert the +#: same `storage: ` rendering the old `Lance` variant produced. No +#: vocabulary or JSON-output change here, unlike the 69d292ce80/ecf1d6aedd +#: rename two refreshes back. #: -#: 69d292ce80 (#534) renames omnigraph's ERROR PROSE — "table" → -#: "dataset"/"entity"/"node type". No serde field renamed, no CLI flag -#: renamed (help text only). But two substrings witan matched on DID -#: vanish: "manifest table version" and "ahead of manifest". See -#: `_RETRYABLE`/`_NEEDS_REPAIR` in omnigraph.py, updated alongside this -#: digest, and the vocabulary tests in tests/test_omnigraph.py. -#: -#: ecf1d6aedd (#538) renames the JSON OUTPUT surface, which is breaking for -#: anyone who parses it: `rows_loaded` → `entities_loaded`, `total_rows` → -#: `total_entities`, `tables` → `nodes`/`edges`, `table_key` → -#: `entity_kind` + `type_name`. witan is not such a caller — it runs the CLI -#: WITHOUT `--json` (deliberately; see the "DO NOT ADD --json" note in -#: `OmnigraphClient.change`) and classifies on stderr prose, and its own -#: `rows_loaded` key is computed in `witan.server.merge_store`, not read -#: back from omnigraph. Anything that starts passing `--json` inherits this -#: rename. -#: -#: ★ AND `edge` MOVED THREE TIMES WHILE THIS WAS BEING WRITTEN — fe5ef3c904… -#: (what CI fetched at 15:36Z, mid-republish), 1fe062b436…, then the triple -#: below, inside 75 minutes. That is the cost of the moving tag, not a mishap: -#: upstream merges several times a day and each push republishes `edge`, so a -#: digest here can be stale before CI runs. Expect to refresh this on a red -#: witan-code job rather than on a schedule, and prefer a real `v` -#: tag the moment 0.10.x has one (there is no v0.10.0 release yet, which is -#: the only reason this is still on `edge`). +#: ★ AND `edge` MOVED THREE TIMES WHILE THE PRIOR TRIPLE WAS BEING WRITTEN — +#: see the git history of this comment for that episode. That is the cost of +#: the moving tag, not a mishap: upstream merges several times a day and each +#: push republishes `edge`, so a digest here can be stale before CI runs. +#: Expect to refresh this on a red witan-code job rather than on a schedule, +#: and prefer a real `v` tag the moment 0.10.x has one (there is no +#: v0.10.0 release yet, which is the only reason this is still on `edge`). #: #: The digests below were taken by downloading all three tarballs and hashing #: them locally, then cross-checking each against the release's published @@ -166,13 +156,13 @@ #: macos-arm64 69f78c93e661e8ea2b92deafe6330650a0921a003c2099b75b226482a90dc03e _OMNIGRAPH_ASSET_SHA256: dict[str, str] = { "omnigraph-linux-x86_64.tar.gz": ( - "8ecabdbc3a11d60716f569b32de6710834ddcbba328c2342b77f5c529bb7bc4f" + "68099e33941cc5c252f36d4c2a26f1dfff6b28e4eb627ba1f7bf098856d34349" ), "omnigraph-linux-arm64.tar.gz": ( - "aef871eeb070532947beee0f7644848552f59bbc8d4100a4bdad6d760acef647" + "d0e42176625584370a26c8a6ca43bc24447ea822483c46d971807475a5a5782a" ), "omnigraph-macos-arm64.tar.gz": ( - "75b3bd0ab4ccfd46af9e70536e8779cbb3af322ab008aa88a2712f14ce9d069e" + "f30f37f0ad8084ed5b26ce5163ebac04554c89bec4259aa1763826bc1efe215f" ), } _VERSION_RE = re.compile(r"\d+\.\d+\.\d+") From 0ae258dd39548072ca99cc88f195c68027f92f5f Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Fri, 21 Aug 2026 10:02:23 -0400 Subject: [PATCH 5/5] chore: release witan-core 0.28.0, witan-council 0.21.0, witan-code 0.13.5 witan-core 0.28.0 carries the refreshed omnigraph digest. Load-bearing, not incidental: 0.27.0 pins the moved edge digest, so a fresh install resolving it gets a `witan setup` / `witan-code setup` that fails the checksum and installs no binary at all. witan-council 0.21.0 ships the project/trace/session-show fix and moves its witan-core floor to >=0.28 for the same reason. witan-code 0.13.5 moves the same floor with no code change of its own. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015DWYCk5hnBjCiwXQPuGok1 --- mcp/servers/witan-code/CHANGELOG.md | 9 +++++++++ mcp/servers/witan-code/pyproject.toml | 15 ++++++++++++--- mcp/servers/witan/CHANGELOG.md | 24 ++++++++++++++++++++++++ mcp/servers/witan/pyproject.toml | 20 +++++++++++--------- packages/witan-core/CHANGELOG.md | 21 +++++++++++++++++++++ packages/witan-core/pyproject.toml | 4 ++-- uv.lock | 6 +++--- 7 files changed, 82 insertions(+), 17 deletions(-) diff --git a/mcp/servers/witan-code/CHANGELOG.md b/mcp/servers/witan-code/CHANGELOG.md index 99a3b1b0..7afae7fd 100644 --- a/mcp/servers/witan-code/CHANGELOG.md +++ b/mcp/servers/witan-code/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/) (pre-1.0: a MINOR bump may include breaking changes). +## [0.13.5] - 2026-08-21 + +### Changed + +- Raised the `witan-core` floor to `>=0.28` for the refreshed omnigraph + `edge` digest (see witan-core's CHANGELOG) — a version below it fails + `witan-code setup`'s checksum and leaves the indexer with no binary to + shell out to. + ## [0.13.4] - 2026-08-19 ### Changed diff --git a/mcp/servers/witan-code/pyproject.toml b/mcp/servers/witan-code/pyproject.toml index 44fdafdc..873ac87a 100644 --- a/mcp/servers/witan-code/pyproject.toml +++ b/mcp/servers/witan-code/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "witan-code" -version = "0.13.4" +version = "0.13.5" description = "witan-code — tree-sitter code graph + cross-repo bridge (mounts under `witan code`)" readme = "README.md" license = "BSD-3-Clause" @@ -78,7 +78,16 @@ dependencies = [ # published, so anything resolving below 0.24 would silently install # without sentry-sdk (or error on the unknown extra) rather than getting # Sentry reporting at all. - "witan-core[cli,remote,observability,sentry]>=0.24,<1", + # + # ★ >=0.28 IS NOT ABOUT AN IMPORT, same exception witan-council's floor + # documents: witan-core 0.28.0 carries the current omnigraph `edge` + # digest, and every version below it fails `witan setup`'s checksum and + # installs no binary at all — indexer tests skip without one + # (tests/conftest.py) and a real indexer run has nothing to shell out to. + # Expect this floor to keep moving while omnigraph 0.10.0 stays on the + # moving `edge` tag; see + # packages/witan-core/witan_core/omnigraph_install.py. + "witan-core[cli,remote,observability,sentry]>=0.28,<1", "fastmcp>=3.4.2,<5", "cyclopts>=4,<5", # [targets.] override models (config.py) — also transitively pulled @@ -162,7 +171,7 @@ testpaths = ["tests"] packages = ["witan_code"] [tool.bumpversion] -current_version = "0.13.4" +current_version = "0.13.5" allow_dirty = true [[tool.bumpversion.files]] diff --git a/mcp/servers/witan/CHANGELOG.md b/mcp/servers/witan/CHANGELOG.md index add5623c..44221b9c 100644 --- a/mcp/servers/witan/CHANGELOG.md +++ b/mcp/servers/witan/CHANGELOG.md @@ -6,6 +6,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/) (pre-1.0: a MINOR bump may include breaking changes). +## [0.21.0] - 2026-08-21 + +### Fixed + +- **`witan project show`, `witan trace show`, and `witan session list` no + longer crash against a deployed target.** All three bypassed the MCP tool + layer with `s.client.read(...)` to reach queries with no dedicated tool — + correct for the in-process local server, where `s.client` is a real + omnigraph client, but `RemoteServerProxy` has no `client`: `__getattr__` + handed back a plain dispatch closure for that name, and `.read(...)` on it + raised `AttributeError: 'function' object has no attribute 'read'`. + + Routed through existing tools instead — `workflow_project_get_blockers`, + `workflow_trace_get`, and a new `include_superseded` flag on + `workflow_session_list` (for `session list`'s dedupe view, the one caller + that wants superseded rows back) — which dispatch correctly against either + target. Regression-tested end to end against a real `RemoteServerProxy`. + +### Changed + +- Raised the `witan-core` floor to `>=0.28` for the refreshed omnigraph + `edge` digest (see witan-core's CHANGELOG) — a version below it fails + `witan setup`'s checksum and leaves the CLI with no binary at all. + ## [0.20.0] - 2026-08-20 ### Fixed diff --git a/mcp/servers/witan/pyproject.toml b/mcp/servers/witan/pyproject.toml index 8e4b98c2..8d3f2849 100644 --- a/mcp/servers/witan/pyproject.toml +++ b/mcp/servers/witan/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "witan-council" -version = "0.20.0" +version = "0.21.0" description = "witan — agent memory, planning, and collaboration graph (work-coordination layer + umbrella CLI)" readme = "README.md" license = "BSD-3-Clause" @@ -38,12 +38,14 @@ dependencies = [ # Hence >=0.21 below. Nothing in CI catches this — read the sentence above # before adding a witan_core symbol, because the tests will not tell you. # - # ★ >=0.27 IS THE ONE ENTRY HERE THAT IS NOT ABOUT AN IMPORT. witan-core - # 0.27.0 refreshed the pinned omnigraph asset digests after upstream moved - # the `edge` tag; 0.26.0 carries the stale ones, so `witan setup` against it - # fails the checksum and installs no binary at all — and witan/server.py - # bootstraps a graph at import time, so the CLI is unusable rather than - # degraded. Same consequence as a missing symbol, different cause. + # ★ >=0.28 CONTINUES THE ONE ENTRY HERE THAT IS NOT ABOUT AN IMPORT. `edge` + # moved again the day after 0.27.0's refresh; witan-core 0.28.0 carries the + # new digest, and every version below it fails `witan setup`'s checksum and + # installs no binary at all — witan/server.py bootstraps a graph at import + # time, so the CLI is unusable rather than degraded. Same consequence as a + # missing symbol, different cause. Expect this floor to keep moving while + # omnigraph 0.10.0 stays on the moving `edge` tag; see + # packages/witan-core/witan_core/omnigraph_install.py. # # (>=0.21 for remote.oidc.SessionLife, imported at module scope by # witan/remote/oidc.py and re-exported in its __all__; @@ -120,7 +122,7 @@ dependencies = [ # failure mode. No AttributeError, no test failure — a published # witan-council on 0.24 just quietly ships the bug this floor exists to # keep fixed. - "witan-core[cli,remote,observability,sentry]>=0.27,<1", + "witan-core[cli,remote,observability,sentry]>=0.28,<1", "pyyaml>=6,<7", "tomli-w>=1,<2", # Direct import in witan/remote/oidc.py for the CLI's OIDC device-code @@ -165,7 +167,7 @@ packages = ["witan"] "schema" = "schema" [tool.bumpversion] -current_version = "0.20.0" +current_version = "0.21.0" allow_dirty = true [[tool.bumpversion.files]] diff --git a/packages/witan-core/CHANGELOG.md b/packages/witan-core/CHANGELOG.md index dad8d966..a67558ac 100644 --- a/packages/witan-core/CHANGELOG.md +++ b/packages/witan-core/CHANGELOG.md @@ -6,6 +6,27 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/) (pre-1.0: a MINOR bump may include breaking changes). +## [0.28.0] - 2026-08-21 + +### Changed + +- **Refreshed the pinned omnigraph `edge` asset digests** to the + 2026-08-21T00:11Z build (through upstream `62a9c3fe6b`). The tag had moved + again the day after 0.27.0's refresh, so `install_omnigraph` refused the + download — correctly — and `witan-code` CI failed with no binary at all + (agent-kit#272). All three tiers move together, verified by hashing each + tarball locally and cross-checking the release's published `.sha256` in the + same sitting. Version still reports 0.10.0 and internal-schema still 6, so + no store rebuild. + + The six commits in between are all storage-layer typed-failure work + (`OmniError::Lance(String)` became `OmniError::Storage(StorageFailure)`, + behind RFC-0038). Checked for the two things that would matter here — the + `_RETRYABLE`/`_NEEDS_REPAIR`/`_PRECONDITION_FAILED` substrings and the + `"storage: "` prose prefix `_classify_cli_error` keys on — and neither + renamed; upstream's own new tests assert the same `storage: ` + rendering the old `Lance` variant produced. + ## [0.27.0] - 2026-08-20 ### Changed diff --git a/packages/witan-core/pyproject.toml b/packages/witan-core/pyproject.toml index ddeb56a8..6ae8aa12 100644 --- a/packages/witan-core/pyproject.toml +++ b/packages/witan-core/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "witan-core" -version = "0.27.0" +version = "0.28.0" description = "Shared core for the witan MCP servers (witan-council + witan-code)" readme = "README.md" license = "BSD-3-Clause" @@ -66,7 +66,7 @@ build-backend = "hatchling.build" packages = ["witan_core"] [tool.bumpversion] -current_version = "0.27.0" +current_version = "0.28.0" allow_dirty = true [[tool.bumpversion.files]] diff --git a/uv.lock b/uv.lock index bcbad6b4..f1d773c3 100644 --- a/uv.lock +++ b/uv.lock @@ -2273,7 +2273,7 @@ wheels = [ [[package]] name = "witan-code" -version = "0.13.4" +version = "0.13.5" source = { editable = "mcp/servers/witan-code" } dependencies = [ { name = "agent-config-kit" }, @@ -2339,7 +2339,7 @@ test = [ [[package]] name = "witan-core" -version = "0.27.0" +version = "0.28.0" source = { editable = "packages/witan-core" } [package.optional-dependencies] @@ -2413,7 +2413,7 @@ test = [ [[package]] name = "witan-council" -version = "0.20.0" +version = "0.21.0" source = { editable = "mcp/servers/witan" } dependencies = [ { name = "agent-config-kit" },