Skip to content

CLOUD-312 row 8 retires: mcp-allow-check --session goes through the door, and row 10 cannot - #707

Merged
wenzowski merged 1 commit into
mainfrom
claude/cloud-9xx-bundle-g-yn29zv
Aug 26, 2026
Merged

CLOUD-312 row 8 retires: mcp-allow-check --session goes through the door, and row 10 cannot#707
wenzowski merged 1 commit into
mainfrom
claude/cloud-9xx-bundle-g-yn29zv

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

Refs CLOUD-312. Every key this commit serves is already Done, so each is declined explicitly rather than left to the automation:

DO-NOT-CLOSE CLOUD-312
DO-NOT-CLOSE CLOUD-898

Row 8 of CLOUD-312's ten-guard wave retires, taking the tally to 7 of 10. It is a small change on purpose — 37 lines added, 9 removed — and most of the added lines are the comment recording why two columns are absent.

The move

mise-tasks/mcp-allow-check.sh --session goes from a direct UserPromptSubmit registration in .claude/settings.json to a [[hook.handler]] row in batten.toml, beside mcp-attach-check, which went through the same door first as the cheapest honest proof of it.

The script and its bats suite both stay — a handler destination keeps its program by intent — so no cases retire and no conserves arms are owed. What moves is the dispatch, and with it the four things the door enforces that no dispatched program can enforce for itself: the bound, central fail-open, a stated output shape, and one reply per call.

The argument is the point of the row. This gate's scope split lives on --session: three predicates are pure functions of the committed settings and belong to the commit, while the fourth reads which servers this session attached, which is a property of the world. Only this registration passes the flag, so moving it here puts that split in the one committed authority instead of spelling it in a settings file the gate itself cannot read.

Two things decided rather than defaulted

No matcher, deliberately. Row 5 needed one because pre-tool fires per tool call, and an unnarrowed handler there doubled the mediated cost of every call it was silent on — measured 41.5 ms against a 21 ms path. user-prompt-submit fires once per prompt and carries no tool, so there is nothing to narrow, and a matcher would read as a restriction while expressing none. The absence is the statement.

Its exit 2 is safe through the door, and that is a property of the door rather than luck. The script exits 2 for could-not-look — unparseable settings, or no jq on the PATH a by-path hook inherits — and 2 is the deny code. A refusal at user-prompt-submit is demoted to advice (lib.rs: "a refusal is demoted to advice on a moment that cannot carry one"), so an unreadable settings file reports and never costs the author their prompt. Registered directly, that same exit was whatever the host chose to make of it.

timeout_ms is measured rather than defaulted, like its sibling's: warm, the predicates are jq over one file plus a read of the generated MCP config and the client's logs, returning in tens of milliseconds. 2000 is two orders of headroom.

Row 10 is NOT here, and it is not portable

CLOUD-312's table lists row 10 (.claude/hooks/session-start.sh) as a handler destination with blockers "none, but see the bound below". The bound — timeout_ms against the 24s cold mise install — is real but is not the blocker.

session-start.sh:160 is step batten-build mise run install:local: this hook is what puts the batten binary on PATH. A [[hook.handler]] is dispatched by batten hook, so on a cold container the sequence is no binary → nothing dispatches the handler → install:local never runs → still no binary. A fixed point, not a slow path; every fresh clone would start unprovisioned. A bound above the cold path solves duration and leaves existence untouched.

So row 10's destination is "stays a direct registration" — the same shape as row 5's correction, in the opposite direction. Recorded on CLOUD-312, along with the consequence for the campaign's own end-state test: row 10 becomes a permanent remainder rather than a pending one, so predicate 2 needs a key for it to name. Filing that row is a scoping decision about this campaign's Done and belongs to the board owner, not to this PR.

Verification

  • mise run test green on this tree; mise run verify green before readying
  • hooks-wiring-check green at 24 registrations across 5 harnesses, with the direct registration and the DECLARED row both gone — wiring-declaration-stale and wiring-sibling-command gate both directions, so neither half of the move can be forgotten
  • mcp-allow-check itself green over this repo's own settings, through the pre-commit gate
  • hook-matcher-check green: all 16 declared verbs still reach the engine through the one PreToolUse entry

Tally: 6 of 10 retired before this PR, 7 of 10 after it. Row 7 (board-write-record into core — unblocked now that CLOUD-919 is Done) and row 9 (CLOUD-892's Stop surface, 1,412 lines, the one closing key still open) remain. Row 10 is reclassified rather than pending. CLOUD-312 stays open regardless: rows 11–13 outlive the wave.


Generated by Claude Code

@linear-code

linear-code Bot commented Aug 26, 2026

Copy link
Copy Markdown
CLOUD-312 The engine is the pre-tool entry point; the shell guards retire behind it

Why

The pre-commit layer and CI are already adjudicated by the engine reading the committed authority. The agent tool-call layer is not: .claude/settings.json wires seven PreToolUse entries — gh-guard, ready-guard, issue-guard, run-shape-guard, memory-guard (twice, once per matcher), claim-guard — every one a mise run of a shell task carrying its own decision table, and batten hook appears in that file zero times.

Two implementations of one policy is two authorities for one fact, and the divergence is silent. A rule added to batten.toml does not reach the tool call, and a guard's table cannot be read from the config a reviewer reviews. crates/batten/src/hook.rs is the port of those guards and its own header describes the compatibility path as lasting "while they exist" — this issue is what ends that period.

It also makes the README's three-layer claim true. Today one third of it describes the design rather than the state.

The counts in this section are the pre-wiring state and are kept as the historical baseline, not as current fact. Re-counted 2026-08-20: .claude/settings.json carries thirteen registrations across six events, of which one reaches batten hook — the single PreToolUse entry. The remainder are SessionStart ×2, UserPromptSubmit ×2, Stop ×1, six further PreToolUse shell entries across five matchers, and PostToolUse ×1. CLOUD-713 owns the census that keeps that number honest; CLOUD-777 owns getting the engine onto every surface exactly once.

Mechanism

  • Each PreToolUse entry invokes the engine with the harness adapter for the host. The decision comes from the mediated_call-scoped rows of the resolved config and from nowhere else.
  • Every refusal a retiring guard renders is expressed as a config row with a required reason before that guard is removed. A guard is deleted only once its refusals are reproduced from config.
  • Fail-open posture is preserved end to end: unreadable stdin, an unparseable payload, or a missing binary all resolve to allow, and the existing bypass variables keep working.

Ready

  • Source of truth (§1). The committed batten.toml is the only table a mediated call is judged against. No decision table remains in mise-tasks/.
  • Mechanism as a predicate (§2). Two gates, both exiting 0:
    1. a differential suite replays every payload fixture in the existing guard .bats suites through the engine and asserts the same decision and the same reason text;
    2. a source-level assertion fails if any PreToolUse entry in the settings file invokes a task that carries a decision table.
  • Effect (§3). No new command surface: hook already exists and is already classified. What changes is who invokes it.
  • Output & exit (§5). Every retiring guard's refusal keeps its reason text, which is what the differential suite asserts; the deny channel per host is the one Capabilities declares. Fail-open is preserved end to end — unreadable stdin, an unparseable payload, or a missing binary all resolve to allow — so no failure code Batten can produce is one a host reads as a deny.
  • Commit / bump (§6). featpatch until 0.1.0: below 0.1.0 release-plz bumps the patch whatever the type says.
  • Test obligation (§7). The differential suite in §2, plus the settings-file assertion, both under mise run verify and CI. A guard is deleted only once its fixtures pass through the engine, so coverage never drops below what the retiring guard had.
  • Blockers (§8). Superseded — see "Blockers, re-verified" below, which is the live list. Two rows were named here when this was written; both are resolved and their relations removed, and they are named there with their evidence. Repeating them here would be a blocker citation with no relation behind it, which ready-lint reports as blocker-cited-without-relation — measured on this row 2026-08-22, two violations, caused by removing the relations without editing this sentence. The live blockedBy relations are CLOUD-924 and CLOUD-925, per row rather than campaign-wide.

The gap is measured, not asserted

Counted against main: seven PreToolUse entries, zero invocations of the engine. The port itself is not the missing piece — hook.rs carries six harness adapters over a harness-blind core, its mediated_call matcher, and a per-host capability table — so what remains is the wiring and the config rows that make each retiring guard's refusal reproducible.

One clarification for whoever picks this up, because the neighbouring language invites the wrong move: the table hook must read is the mediated_call-scoped rows of batten.toml, not crates/batten/src/effect.rs. That module classifies Batten's own command surface for the §5 read-only allowlist, and its consumer is spec.rs. Two declared tables, two different objects; importing one into the other would put a classification of Batten's verbs in the path that judges a consumer's shell commands.

Done

main carries the engine as the pre-tool entry point with the differential suite green, no guard-local decision table remains, and CI is green on the merge commit landed by fast-forward.


The remaining inventory, re-counted 2026-08-22 against main (170c7c4)

This section is the campaign's operative content. Everything above it is history: the counts in Why are the pre-wiring baseline, and the ## Ready block's §8 is superseded by Blockers, re-verified below.

What has already changed under this row

  • Registration is finished. CLAUDE_EVENTS carries eight events (hook.rs:1013, UserPromptSubmit added by CLOUD-777) and .claude/settings.json registers batten hook --harness claude-code matcherless on all eight. There is nothing left to register, and no new registration is planned by any row in this bundle. A row proposing one is proposing a second narrowing.
  • The census moved in-process. batten doctor hooks (doctor.rs:225-344) computes the diagnosis from WiringFile and Wiring::registrations, reporting registrations / siblings / merged / merged_surfaces_read and eight stable reason ids — including hook-wiring-merged-registration, so a registration on a $HOME surface the repository does not own is visible. hooks-wiring-check.sh is now the thin caller holding this consumer's DECLARED table (:168-180).
  • The door exists and has a worked example. [[hook.handler]] landed (CLOUD-898) and batten.toml:1912 dispatches mcp-attach-check through it. That guard is therefore already retired from this table — it is dispatched by batten hook, not registered beside it.

The thirteen remaining rows

One row per entry in hooks-wiring-check.sh's DECLARED table. Destination is the load-bearing column: durable policy goes to core/config, and a [[hook.handler]] is used only where an external program intentionally remains. Two of thirteen qualify; assuming every script becomes a handler would move eleven decision tables out of the committed authority and behind a dispatch.

# Event / matcher Command (lines) Owner Destination Blocker & ordering
1 PreTool .*save_issue mise-tasks/issue-search-guard.sh (93) 312 config — a receipt row over the search receipt none; first in the board family
2 PreTool .*save_issue mise-tasks/issue-read-guard.sh (117) 312 config — a receipt row with the recency bound facts::Sourced borrowed from it none; after 1 (shares the matcher and the receipt store)
3 PreTool .*save_issue mise-tasks/board-move-guard.sh (158) 312 config — a receipt row keyed on the issue key none; after 2
4 PreTool .*(subscribe_pr_activity|send_later|create_trigger) mise-tasks/connector-verb-guard.sh (174) 312 config — but the predicate is a tool-name suffix, and no rule kind selects on one today; [[verb]] names a shell program blocked on CLOUD-924 — no rule kind keys on the tool a call names, and this guard matches by SUFFIX deliberately
5 PreTool ^mcp__ mise-tasks/connector-allow-guard.sh (88) 312 config — needs a connector-grant table in batten.toml; the grants live in .claude/settings.json today blocked on CLOUD-924 (the selector), plus that grant table
6 PreTool Task mise-tasks/fanout-guard.sh (158) 312 config — Field::Prompt exists, but [budget.<name>] is a file-set budget over globs, not a per-call ceiling blocked on CLOUD-925[budget] counts a file set, so a per-call ceiling is inexpressible
7 PostTool .*save_issue|.*save_comment mise-tasks/board-write-record.sh (329) 312 core — it derives a record from a tool response, which is exactly the capture bundle's first consumer ordered after CLOUD-919; porting it first would build a second reader of the response
8 UserPromptSubmit mise-tasks/mcp-allow-check.sh --session (415) 312 handler — reads settings files and MCP client logs, not the envelope; its sibling mcp-attach-check already went this way none; the door is landed
9 Stop mise-tasks/stop-guard.sh (318) + five gates (1,412) 892 config / core CLOUD-892 owns it end to end
10 SessionStart .claude/hooks/session-start.sh (295) 312 handler — it provisions a toolchain and preflights the container. There is no decision table in it to move; it is deliberately synchronous and deliberately loud on failure none, but see the bound below
11 PreTool Bash mise-tasks/run-shape-guard.sh (647) 821 config, partially — Field::RunInBackground landed, so the exemption predicate is expressible CLOUD-613 for the heredoc-binding family; CLOUD-821 owns the row
12 Stop, merged $HOME stop-hook-git-check.sh 605 / 893 out of repo — not ours to port CLOUD-893 owns visibility, CLOUD-605 the identity conflict
13 SessionStart, merged $HOME session-start-git-identity.sh 605 / 893 out of repo — same as 12

Row 10 carries a bound the door does not give for free

[[hook.handler]] imposes a timeout_ms, and this script's whole reason for existing is that a cold mise install inside the MCP client's startup window took 24s. A bound tighter than the cold path turns a fail-open handler into the absence the hook was built to close. So its handler row declares a measured bound, and the migration records the cold measurement beside it — the same standard mcp-attach-check's timeout_ms = 2000 was held to.

Per row, the two obligations this issue has always carried

Unchanged in substance from Mechanism above, restated because the table needs them per row:

  • Differential test. Every refusal the retiring script renders is reproduced from the committed authority before the script is deleted, proved by replaying that script's own .bats fixtures through the engine and asserting the same decision and the same reason text. A handler destination has the same obligation with the door in the path: the fixture goes through batten hook, and the reply is byte-compared.
  • Exact deletion condition. The script, its DECLARED row, and its bats suite go in one change, and only once its fixtures pass through the engine — so coverage never drops below what the retiring guard had. A DECLARED row naming a deleted command already fails as wiring-declaration-stale, and a command with no row already fails as wiring-sibling-command, so both directions of the deletion are gated rather than reviewed.

Blockers, re-verified 2026-08-22 — this supersedes §8 above

  • CLOUD-446 — cleared, Done. The claimed-key lookup it called unreachable from the mediated path is reachable: CLOUD-776 landed the agent-sourced fact channel, and claim-not-raced is its worked instance.
  • CLOUD-461 — cleared, landed (In Review). The advisory channel is on main, and contract-drift retired with it. Its own release is not this row's precondition.
  • New, per row rather than campaign-wide, and filed rather than deferred: rows 4 and 5 are blocked on CLOUD-924 (no rule kind keys on the tool a mediated call names); row 5 additionally needs a connector-grant table in batten.toml; row 6 is blocked on CLOUD-925 ([budget] counts a file set, so a per-call ceiling is inexpressible); row 7 is ordered after CLOUD-919. Nothing blocks rows 1, 2, 3, 8, 10.
  • Two rows first named here as blockers are Done, and naming them would have been the defect this table gates against. CLOUD-684 (MCP allow rules naming labels host servers never register under) and CLOUD-734 (re-projecting the grants at SessionStart) are both closed. What row 5 actually lacks is a config surface, which is why CLOUD-924 exists and those two do not appear above.

Stating them per row is the correction: a single campaign-wide blockedBy is what let this row sit blocked on a capability that only one of its thirteen entries needed.

The end-state test

Three predicates, all decidable by machinery that exists:

  1. Exactly one Batten registration per supported event, per harness — doctor hooks already fails hook-wiring-event-registered-n-times and hook-wiring-event-unregistered, and hook-wiring-matcher-narrows on any matcher at all.
  2. No unmanaged sibling commanddoctor hooks reports siblings == 0 and merged == 0, or every remainder is a DECLARED row naming a key that is still open. A row naming a closed key already fails, which is what keeps this from becoming a permanent waiver list.
  3. Every remaining dispatched behaviour is declared in committed configuration and validated from it — each surviving program is a [[hook.handler]] row in batten.toml with a declared bound, and its behaviour is pinned by a differential case run through the door. Nothing reaches a hook surface that the committed authority does not name.

Done is the three above holding together, with main green: not "the scripts are gone", because a deleted script whose refusals nothing reproduces is a coverage loss wearing a retirement's clothes.

CLOUD-898 `[[hook.handler]]`: one door to the hooking surface, so the contract is enforced whatever is dispatched behind it

Why

batten hook registers on every surface of every harness, fast, even for a no-op — and then thirteen bash hooks are registered beside it in .claude/settings.json, each carrying its own contract, its own timeout handling, its own fail-open posture and its own output shape. None of them measured. The engine is one voice among fourteen at the boundary it is supposed to own.

The fix is not to rewrite each script. It is to make batten hook decide what to run and how, including bash, so the only path to the hooking surface is through Batten and hooking is as consistent across harnesses as it can be.

Why [[hook.action]] cannot serve

CLOUD-91's surface already dispatches a declared command on an event, and its module doc states three properties as deliberate — each exactly wrong for a handler:

  • fire returns (), and "an action can never change the answer" is called the load-bearing property, structural rather than promised;
  • the child's streams are discarded as a rule-4 secret risk;
  • pre-tool is refused outright.

Those are right for a side effect and wrong for a participant. So action keeps its guarantee unchanged and this is a second noun. A reader choosing between them has one question — may this change the answer? — and the kind is the answer.

pre-tool is admitted, and not by symmetry. action's first objection does not transfer: a side effect there runs before a possible deny, which inverts what a mediated gate is for, but a handler is part of that decision. Its second objection transfers intact and is honoured: run_hook's hot path touches no config when a pre-tool payload carries neither a command nor a write, so handler resolution takes CLOUD-460's narrowing — the shape reads_prospective uses — and a call no handler selects for still does less work than --help.

The four things the door enforces, each previously re-derived differently in every script:

  1. A bound. Declared per handler, imposed by the parent. stop-guard hand-rolls timeout 1s cat for exactly this and says so; the rest have none.
  2. Fail-open, centrally. Spawn failure, timeout, an undefined exit code — all could not look, which allows. Each script spelled this itself, and a missed path was a hook that could refuse because it was broken.
  3. A stated output shape. stdout on a passing exit is advisory text; a refusal's reason is on stderr. Anything else is a contract violation, reported and never forwarded. This is also what retires the "hooks announce success" defect (CLOUD-891) as a property rather than a habit.
  4. One reply per call. Handler results merge; the host reads one document.

stdout is interpreted, never forwarded — the rule-4 answer and the portability answer at once. action must discard its child's streams; a handler's output is the channel, so it is read into Batten's types and Batten re-renders per harness. A handler speaks to Batten, in Batten's vocabulary, never to the host — so it cannot emit a host decision document, and a repository's hooks behave the same on every harness rather than on the one whose JSON its author happened to write. A handler that tries anyway is reported rather than passed along.

Refinement — Ready

Refinement gate: Definition of Ready & Done. This body carries only specializations.

  • Source of truth (§1). batten.toml's [hook] table, which CLOUD-91 already established as the place a repository attaches to hook events. One authority; .claude/settings.json keeps only the batten hook registration.
  • Computable predicate (§2). The kind, its validator (empty id, unknown event, unrecognized, empty argv, zero timeout, duplicate id — all refused at load), and the dispatch. selects is the narrowing and is asserted, not intended.
  • Effect (§3). hook is already classified; a handler is ambient by construction — it spawns with the calling user's authority, exactly as a command row does, and that is why the contract rather than the code is what constrains it.
  • Generated artifacts (§4). schema/batten.schema.json is derived and drift-gated.
  • Output & exit (§5). §7's table with no per-kind exception: 0 pass, 1 the handler reported, 2 refusal, anything else outside the contract. Pointer-only — a violation names the handler id and what it did, never a byte the handler wrote, since its streams are the widest secret surface on this path.
  • Commit / bump (§6). feat → patch until 0.1.0.
  • Test obligation (§7). A payload no handler selects for does less work than --help; a handler that hangs is killed at its bound and the turn still ends; an undefined exit allows and reports; a host decision document on stdout is a violation, not an advisory; two refusals yield the first rather than a merged reason; every violation variant's pointer carries no handler output. Mutation coverage per CLOUD-418.
  • Blockers (§8). None — the advisory channel it merges into is already on this branch.

Done

.claude/settings.json carries one entry per event, batten.toml carries the handlers, and the contract holds identically whether what runs behind the door is Rego, bash, or something nobody has written yet.

Review in Linear

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ccc87f5c-92bc-42e3-8058-1ddbaae13da2

📥 Commits

Reviewing files that changed from the base of the PR and between 6489655 and 9d1371f.

📒 Files selected for processing (3)
  • .claude/settings.json
  • batten.toml
  • mise-tasks/hooks-wiring-check.sh
💤 Files with no reviewable changes (2)
  • mise-tasks/hooks-wiring-check.sh
  • .claude/settings.json

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

The direct UserPromptSubmit hook for mcp-allow-check.sh --session was removed from .claude/settings.json. A Batten user-prompt-submit handler now runs the session-scoped check with a 2-second timeout and non-blocking advice behavior for unreadable settings. The hook wiring validation no longer lists the script as an exception.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: moving mcp-allow-check --session through the Batten hook handler. It also records why row 10 remains unchanged.
Description check ✅ Passed The description directly explains the handler migration, its configuration, verification results, and the decision to retain row 10 as a direct registration.
Full details: Docstring Coverage

Explanation

No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0 files. (1 skipped: 1 unsupported.)

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/cloud-9xx-bundle-g-yn29zv

Comment @coderabbitai help to get the list of available commands.

@wenzowski
wenzowski marked this pull request as ready for review August 26, 2026 01:45
`mcp-allow-check --session` moves from a direct `UserPromptSubmit` registration to
a `[[hook.handler]]` row, beside `mcp-attach-check`, which went through the same
door first as the cheapest proof of it. The script and its bats suite both stay —
a handler destination keeps its program by intent — so no cases retire and no
`conserves` arms are owed.

The argument is the point of the row. This gate's scope split lives on
`--session`: three predicates are pure functions of the committed settings and
belong to the commit, while the fourth reads which servers THIS session attached,
which is a property of the world. Only this registration passes the flag, so
moving it here puts that split in the one authority instead of spelling it in a
settings file the gate cannot read.

No matcher, deliberately. Row 5 needed one because `pre-tool` fires per tool call
and an unnarrowed handler doubled the mediated cost of every call it was silent on.
`user-prompt-submit` carries no tool, so there is nothing to narrow and a matcher
would read as a restriction while expressing none.

Its `exit 2` is safe through the door, and that is a property of the door rather
than luck: the script exits 2 for could-not-look — unparseable settings, or no `jq`
on the PATH a by-path hook inherits — and 2 is the deny code, but a refusal at
`user-prompt-submit` is demoted to advice, so an unreadable settings file reports
and never costs the author their prompt. Registered directly, that exit was
whatever the host chose to make of it.

`timeout_ms` is measured rather than defaulted, like its sibling's: warm, the
predicates are `jq` over one file plus a read of the generated MCP config and the
client's logs, returning in tens of milliseconds. 2000 is two orders of headroom.

Both deletion directions stay gated: `wiring-declaration-stale` refuses a
`DECLARED` row naming a deleted command and `wiring-sibling-command` refuses a
command with no row, so neither half of the move can be forgotten. Green at 24
registrations across 5 harnesses.

Row 10 is NOT in this commit and is not portable: `session-start.sh` is what runs
`install:local`, so a handler dispatched by `batten hook` could never run on a cold
container that has no `batten` yet. The bootstrap deadlock and what it means for
this campaign's Done predicate are recorded on CLOUD-312.

Refs: CLOUD-312
Refs: CLOUD-898
@wenzowski
wenzowski force-pushed the claude/cloud-9xx-bundle-g-yn29zv branch from 9d1371f to 1e22bf0 Compare August 26, 2026 02:47
@sonarqubecloud

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 1e22bf0 into main Aug 26, 2026
5 of 6 checks passed
@wenzowski
wenzowski deleted the claude/cloud-9xx-bundle-g-yn29zv branch August 26, 2026 03:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant