Skip to content

fix(run): make run-configuration disambiguation work on Windows - #653

Merged
kshivang merged 2 commits into
risa-labs-inc:devfrom
Vidhaankhare16:fix/run-config-windows-paths
Sep 15, 2026
Merged

kshivang merged 2 commits into
risa-labs-inc:devfrom
Vidhaankhare16:fix/run-config-windows-paths

Conversation

@Vidhaankhare16

@Vidhaankhare16 Vidhaankhare16 commented Sep 14, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Run-configuration name disambiguation is dead code on Windows. makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath on a literal /, but a stored filePath is an OS-native absolute path: DesktopMainFunctionDetector assigns file.absolutePath (DesktopMainFunctionDetector.kt:139), which is backslash-separated on Windows. split("/") therefore returns a single part, the parts.size >= 2 guard never passes, and two configurations that share a name in different directories stay byte-identical in the Run dropdown. Disambiguation is the entire purpose of both functions, so on Windows the feature does not work at all.

What this changes

Both functions now split on / and \, the leading-separator strip becomes trimStart('/', '\\'), and the project name comes from extractFileName() rather than substringAfterLast('/'). plugin-path-utils/PathUtils.kt describes itself as "the single source of truth for path utilities used across all modules" and its extractFileName is documented as handling both separators. RunConfiguration.toShortNameWithProject already builds the initial label with that helper; this file was the one place re-deriving the same values with a hardcoded separator and disagreeing with it. Display labels still join with / so a label reads the same on every platform, which is existing behaviour and unchanged.

A second defect, latent until the first is fixed

projectName was projectPath.substringAfterLast('/'). On a Windows path there is no /, so substringAfterLast returns the receiver unchanged and the project name becomes the entire absolute path, producing main (app/Main.kt [C:\Users\<name>\myproject]). It is unreachable today only because the value is consumed inside the parts.size >= 2 branch that never executes on Windows. Fixing the split alone would have surfaced a user-path leak in the UI, so both halves are in one change.

Why the tests target the functions directly

Both functions changed from private to internal. These are pure string transformations, and the point of the suite is to exercise Windows path shapes on every CI leg. The public entry points are scanProject, which walks a real directory tree, and the settings load path, which reads ~/.boss/run-configurations.json; neither can present a C:\... path to the code under test on a Linux or macOS runner, so routing through them would leave the Windows branch unexercised on two of the three legs. There is no filesystem or platform setup to eliminate here, which is what makes direct testing the right granularity rather than a shortcut. The repo has no @VisibleForTesting convention and internal fun is used throughout desktopMain.

Coverage

Ten cases. Three pin the Windows behaviour, four pin the existing POSIX behaviour so the change cannot regress it, and three cover edges a plausible rewrite would get wrong: a file directly in the project root, mixed separators, and a project path with a trailing separator.

The root-level case is the one that earns its place. trimStart looks redundant beside a split that already accepts both separators, but without it a root-level file relativises to \Main.kt, splits to ["", "Main.kt"], clears the size >= 2 guard on the empty segment and produces a label of main (/Main.kt [myproject]). I verified that by transcribing both variants rather than assuming it.

Precedence and scope

One production file, two functions, each with exactly one caller (DesktopRunConfigurationManager.kt:65 and :138). No persistence format change: makeStoredNamesUnique already rewrote names on load, and only the computed label differs. No behaviour change on POSIX. No new dependency; extractFileName is already imported by 13 other files in composeApp, making this the 14th.

#594 is adjacent but separate: it covers shellEscape and the hardcoded /tmp fallbacks in DesktopMainFunctionDetector, which is command generation rather than display-name disambiguation, and it already has PR #595. No function-level overlap with this change. At the time of writing no open PR touches DesktopRunConfigurationManager.kt, checked with paginated file lists rather than the capped default.

Attribution

Authored by @Vidhaankhare16.

Known limits

The Run dropdown itself was not observed. A fork build stops at sign-in without store credentials, so the claim here is about the generated label strings and not a screenshot. makeStoredNamesUnique splits the full absolute path rather than a project-relative one, so its size >= 2 guard is effectively always satisfied for an absolute path; that is pre-existing behaviour and this change does not alter it.

Validation

./gradlew --no-build-cache :composeApp:desktopTest ktlintCheck detekt on head 03ba543b, base a3f4b7f0, Windows 11, JDK 17.0.20.1+1. 4694 tests across 490 classes, 15 skipped, 2 failures. Both failures are environmental on this machine and unrelated to this change: DesktopFileScannerTest > directoryHasChildren follows a symlink to a non-empty directory and DiscardDownloadContainmentTest > a symlink inside staging pointing out is refused, each failing with java.nio.file.FileSystemException: A required privilege is not held by the client, which is the Windows symlink-creation privilege this machine does not grant. RunConfigurationNameSeparatorTest itself is 10 passed, 0 failed, 0 skipped. :composeApp:ktlintCheck and :composeApp:detekt both executed and passed; the only failing task was :composeApp:desktopTest. An earlier six-case version of this file, run against the unfixed code, produced 3 failures and 3 passes, the three failures being exactly the Windows cases, so those cases discriminate rather than passing vacuously. The four cases added afterwards (root-level Windows, root-level POSIX, mixed separators, trailing separator) were not run against the unfixed code. The root-level case was instead checked by transcribing the function with and without the leading-separator trim, which yields main (/Main.kt [myproject]) without it. Not run on macOS or Linux.

Manual checks remaining

Open two run configurations with the same name in different directories of one project on Windows, and confirm the dropdown shows distinct entries and that neither label contains the absolute project path.

No merge or auto-merge is authorized.

@kshivang

Copy link
Copy Markdown
Contributor

@claude Please review the current head for Windows/POSIX run-configuration label correctness and regression-test coverage.

@claude

claude Bot commented Sep 14, 2026 •

Copy link
Copy Markdown

Claude finished @kshivang's task in 6m 13s —— View job


Review: Windows/POSIX run-configuration label correctness

  • Read the diff on head 03ba543
  • Verify makeNamesUnique / makeStoredNamesUnique label correctness on Windows and POSIX
  • Check extractFileName semantics against the new call sites
  • Audit the new test file for discriminating coverage and gaps
  • Check for other hardcoded separators in the run package
  • Post review

Verdict

The diagnosis is right and the fix is correct. DesktopMainFunctionDetector.kt:139 does store file.absolutePath, split("/") on C:\…\Main.kt yields one part, and parts.size >= 2 never passes - so both functions were no-ops on Windows. Switching to split('/', '\\') + trimStart('/', '\\') + extractFileName() fixes it, and routing the project name through the repo's canonical helper removes the one place that re-derived those values with a hardcoded separator while RunConfiguration.toShortNameWithProject (RunTypes.kt:112) already used the helper.

I hand-traced all ten test cases through the new code and they all produce the asserted labels. I also traced the four cases the PR flags as not mutation-verified against the pre-fix code - three of them do discriminate, so the honesty note in the description understates the suite:

Test Fails on pre-fix code?
a windows file at the project root keeps its name yes - both entries stay main (Main.kt [myproject])
mixed separators are handled yes - old code yields main (\app/Main.kt [myproject]), the backslash surviving removePrefix("/")
a project path with a trailing separator omits the project suffix yes - split("/") gives one part, names unchanged
a posix file at the project root keeps its name no - pure regression guard, which is its job

Two findings worth acting on before merge, both about the stored path rather than the scan path, plus notes.


1. makeStoredNamesUnique drops the [Project] suffix, and that is newly reachable on Windows

DesktopRunConfigurationManager.kt:114 replaces the whole trailing group with ($parentAndFile), where makeNamesUnique:184 rebuilds it as ($parentAndFile$projectSuffix). The regex \([^)]+\)$ matches (Main.kt [myproject]) in full, so the bracket is consumed and not restored.

That asymmetry is pre-existing on POSIX, but on Windows the branch never executed before this PR, so the change makes it reachable for the first time:

stored name : main (Main.kt [myproject])   x2, in app\ and lib\
before       : main (Main.kt [myproject]), main (Main.kt [myproject])   (the bug)
after        : main (app/Main.kt),         main (lib/Main.kt)           ([myproject] gone)

Stored names really do carry the bracket - history entries come from the detected list via BossTopRunBar.kt:98/:139, whose names are toShortNameWithProject output. And the rewrite is not display-only as the description suggests: loadSettingsSync assigns the renamed configs to _currentSettings (:68), so the next saveSettings() persists the stripped label. The user-visible effect is a run history where some entries identify their project and others no longer do.

The suite can't catch this because both stored-name tests (:107, :120) use bracket-free names ("main (Main.kt)"), which is not the shape the file actually holds. Worth one test with a bracketed name pinning whichever behaviour is intended, and I'd argue for preserving the bracket for parity with makeNamesUnique. Fix this →

2. The splits are missing .filter { it.isNotEmpty() }, unlike the sibling idiom

The established form for exactly this job is two files over, in DesktopMainFunctionDetector.detectModuleName:

.split(File.separator, "/", "\\")
.filter { it.isNotEmpty() } // Remove empty parts

Without the filter, an interior doubled separator puts an empty segment into takeLast(2):

C:\p\app\\Main.kt  ->  [C:, p, app, "", Main.kt]  ->  takeLast(2) = ["", "Main.kt"]  ->  main (/Main.kt)

File.absolutePath won't emit that, but makeStoredNamesUnique reads ~/.boss/run-configurations.json, which is hand-editable, and it splits the raw absolute path with no relativisation to absorb it. main (/Main.kt) is precisely the failure shape the PR's own doc comment at :129-134 guards against in the other function - the leading-separator trim closes it there and nothing closes it here. Cheap hardening, and it makes the two files state the same rule. Fix this →


Lower-priority observations

Trailing-separator project path silently loses the bracket. extractFileName("C:\Users\dev\myproject\") is "", so takeIf { isNotBlank() } gives null and the label is main (app/Main.kt). The relativisation still works, so only the project name is lost. This matches plugin-path-utils/PathUtils.kt:18 ("Paths with trailing separators: Not normalized automatically"), so it is consistent rather than wrong - but the line is already being touched, and projectPath.trimEnd('/', '\\').extractFileName() fixes it in one call. My concern is mainly that the new test at :180 pins the wart as the expected result, which makes it harder to fix later; if it stays, the test name or KDoc should say "accepted" rather than reading as intended behaviour. Fix this →

removePrefix is exact, and fails benignly - which is worth a test. projectPath reaches scanProject from the picker, the CLI and deep links, while filePath is File.absolutePath; on Windows they can differ in drive-letter case or separator style, and then removePrefix is a no-op. The fallback is parts.takeLast(2) of the absolute path, which still yields app/Main.kt and still cannot leak the full path. That is a genuinely good property of takeLast(2) and currently unasserted - the "must not embed the absolute project path" test at :59 only covers the matching-prefix case, so the one input that could plausibly leak is untested.

No idempotency test for makeStoredNamesUnique. It runs on every launch and its output can be persisted, so non-idempotence would mean labels drifting each start. I traced it as stable (rewritten names are distinct, so the next groupBy yields singletons), and stable-but-permanently-ambiguous for x/app/Main.kt + y/app/Main.kt, which both collapse to main (app/Main.kt). Two lines to pin, and it matters more now that the path actually executes on Windows. Fix this →

Accepted, no action: a POSIX directory whose name contains a literal \ now mis-splits (a\b/Main.kt labels as b/Main.kt). That is the documented trade in plugin-path-utils/PathUtils.kt:16 - "Both slash styles are separators, including literal backslashes in POSIX names" - and matching the canonical helper is the right call. Also pre-existing in both functions: the regex no-ops for a name without a trailing (...) group, so a user-renamed configuration is never disambiguated.

Nit: assertEquals(names.size, names.toSet().size, …) at :48 passes the actual on both sides; assertEquals(2, names.toSet().size, …) states the intent more directly. The failure message happens to read correctly either way.


On the design choices

Making the two functions internal is the right call here and the justification in the description holds up: they are pure string transforms with no filesystem access, so C:\… expectations are exactly as meaningful on a Linux runner, and the public entry points (scanProject, which walks a real tree, and loadSettingsSync, which reads ~/.boss/run-configurations.json) could not present a Windows path shape on two of three CI legs. Worth noting that the pure-string implementation is what makes that possible - a File/Path.relativize rewrite would be more robust about prefix matching but would make the Windows branch untestable off Windows, so the current shape is the better trade.

Scope claims check out: makeNamesUnique and makeStoredNamesUnique have exactly one caller each (:138, :65), and git diff origin/dev HEAD over composeApp/**/run/ shows only these two files.

Process note

The branch is based on main, not dev, but targets dev: git log origin/dev..HEAD lists ~30 commits, so merging as-is carries #647, #645, #635, #477 and the v9.5.x bumps into dev alongside this change. That may be how this repo syncs the branches, but it is not what the description implies, and it is worth confirming before anyone merges. No merge or auto-merge attempted, per the PR body.

What I could not verify

I could not execute anything in this environment - Gradle, node and python3 all require approval here - so ktlintCheck, detekt and :composeApp:desktopTest were not re-run, and every label above is a hand trace of the Kotlin string operations rather than a program's output. The author's reported run (10/10 on this class, two unrelated symlink-privilege failures) is consistent with my traces. If you want the linters and tests actually executed on this head, re-run me with Bash approval for ./gradlew.
· branch fix/run-config-windows-paths

@kshivang

Copy link
Copy Markdown
Contributor

@Vidhaankhare16 Your fix addresses a real Windows failure in both scan-time and stored-name disambiguation, and correctly pairs separator handling with the shared project-name helper. The ten regression tests are useful and the host placement is appropriate. I found no substantive correctness/security/regression defect in the two-file patch at 03ba543b0c96f8a9d02bfd33d3dbe9804d3288d2; no agent code repairs were needed and your authorship remains intact.

Local validation on macOS passed: :composeApp:desktopTest --tests '*RunConfigurationNameSeparatorTest' (10 tests, zero failures/errors/skips), :composeApp:ktlintCheck, and :composeApp:detekt, through the shared build lock. The initial sandbox cache-lock failure was resolved by retrying with cache access. Current main/dev still need this fix; integration #651 is confirmed merged. #595 (command quoting/temp paths) and #634 (bridge authorization) are distinct work, not duplicates.

I approved the fork's previously waiting Build & Test workflow. Shell tests, version-check and BOSS AI contracts have passed; platform builds, quality/compatibility and database CI are still pending. The single requested Claude review is also still running: https://github.com/risa-labs-inc/BossConsole/actions/runs/34830325963 . CI: https://github.com/risa-labs-inc/BossConsole/actions/runs/34826750597 . Pending infrastructure/review work is not evidence of an author defect.

Converting to draft as the sweep's bounded validation handoff, not rejecting the implementation. Next steps: inspect the completed Claude review and address valid findings (or document evidence for rejected suggestions), confirm current-head CI passes, recheck against latest dev, then mark ready for a locked squash merge into dev. No source change is requested based on this review. The Windows dropdown visual check remains unperformed: confirm two same-named configurations in different directories show distinct labels without the absolute project path. No application was launched for this review.

@Vidhaankhare16

Copy link
Copy Markdown
Contributor Author

Thanks both. The two substantive findings are right and I have taken them, along with the trailing-separator point and the assertion nit. Posting this before the push so the completed run on 03ba543b stays anchored to the head it actually ran on: https://github.com/risa-labs-inc/BossConsole/actions/runs/34826750597 finished green on all ten jobs, including the three platform builds and pgTAP.

Finding 1, the dropped project bracket, is the one I should have caught myself. The review is right that it is not display-only: loadSettingsSync assigns the rewritten names to _currentSettings and the next saveSettings() persists them. It is also right about why my suite missed it, and the reason matters more than the miss. Both stored-name tests used bracket-free names, which is not the shape the file holds, because stored names are toShortNameWithProject output. So the tests were exercising a fixture that cannot occur rather than the real input. makeStoredNamesUnique now carries the existing bracket across, and there are tests for both platforms using bracketed names.

I want to be explicit that this one is mine and newly reachable: the asymmetry is pre-existing on POSIX, but that branch never executed on Windows before this PR, so my change is what would have shipped it as a user-visible regression on the platform the PR exists to fix.

Finding 2, the empty-segment filter, is taken in both functions, matching DesktopMainFunctionDetector.detectModuleName. One consequence worth flagging rather than leaving for a reader to notice: the filter subsumes the leading-separator trim, because a relative path's leading separator is exactly an empty first segment. I removed the trim rather than keep it as dead belt-and-braces, so the two files now state one rule instead of two. The root-level test still discriminates, since without the filter that case produces the /Main.kt label the trim used to prevent.

On the trailing-separator observation, I agree that pinning the wart makes it harder to fix later, so I fixed the wart instead: the project name is read from projectPath.trimEnd('/', '\\') and the test now expects the bracket. The assertEquals(2, names.toSet().size, ...) nit is taken too.

I also added the two cases the review flagged as unasserted: idempotency of the stored rewrite, and a projectPath that is not an exact prefix of filePath. The second confirms the property the review identified, that takeLast(2) of the absolute path is what keeps the fallback from leaking. Tests go from 10 to 16.

Both regexes moved to object-level vals while I was there. The trailing-group pattern was duplicated across the two functions, and neither needed recompiling per element.

On the process note, I think the base-branch reading is mistaken, and I would rather show the evidence than leave it standing, since unaddressed it is the kind of thing that stops a merge. git rev-list --count origin/dev..HEAD was exactly 1 at review time and is 2 now with the fix commit. The parent of 03ba543b is a3f4b7f0, which is #647 and is itself on dev, so #647 is inherited from dev rather than carried by this branch. None of #647, #645, #635 or #477 appear in origin/dev..HEAD. My reading is that the review compared against a dev from before one of the recent rewrites: dev has been rewritten twice while this branch was open, which is also what put #644 into CONFLICTING. If you see ~30 commits from your side I would like to know, because then I am the one reading it wrong.

Validation on the new head, Windows 11, JDK 17, --no-build-cache: 4700 tests across 490 classes, 15 skipped, 2 failures. Both failures are the known environmental pair, DesktopFileScannerTest and DiscardDownloadContainmentTest, each failing with java.nio.file.FileSystemException: A required privilege is not held by the client, the Windows symlink-creation privilege this machine does not grant. RunConfigurationNameSeparatorTest is 16 passed, 0 failed, 0 skipped. :composeApp:ktlintCheck and :composeApp:detekt both executed and passed. Not run on macOS or Linux.

This is one commit on top rather than a force-push, so 03ba543b and its green run stay intact and the review delta is readable on its own. That leaves the new head needing a CI approval when you have a moment. The Windows dropdown visual check is still unperformed and I am not claiming it.

No merge or auto-merge is authorized.

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.
Addresses the review on 03ba543.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.
@Vidhaankhare16
Vidhaankhare16 force-pushed the fix/run-config-windows-paths branch from 98eb794 to 71b736d Compare September 14, 2026 12:35
@Vidhaankhare16

Copy link
Copy Markdown
Contributor Author

Rebased onto current dev (5a2e8181) and force-pushed, 98eb794c to 71b736da. No source change: this was a base repair only, and I want to state precisely what that does and does not establish.

The branch had gone CONFLICTING without a content conflict. It was based on acfc62b4, and dev was then rewritten so that #661 (5a2e8181) collapsed five of those commits into one. That is what made the PR read as six commits and made GitHub report a conflict. Replaying only my two commits with git rebase --onto origin/dev acfc62b4 drops nothing: it is now 0 behind, 2 ahead, and MERGEABLE.

The content is byte-identical to the head you reviewed. Both file blobs are unchanged across the rebase, 3055e202 for DesktopRunConfigurationManager.kt and 288f4375 for RunConfigurationNameSeparatorTest.kt, and the diff has the same patch-id, cffa6576. Your review delta between 03ba543b and the fix commit is preserved as the second commit.

What I did not do is re-run the suite on this base, so I am not claiming a fresh green. What stands is the run I reported on the previous head: 4700 tests, 490 classes, 15 skipped, the 2 known environmental symlink failures, RunConfigurationNameSeparatorTest 16 passed 0 skipped, plus ktlintCheck and detekt. That result applies to this content because the content did not change. The only thing that changed under it is the base, and the delta from acfc62b4 to 5a2e8181 is four files, ActiveTabsServiceBridge.kt, ProcessIdentityInterceptor.kt, libs.versions.toml and plugin-api-core/build.gradle.kts, none of which this change compiles against. ktlintCheck and detekt are content-only and cannot regress from a base move.

The green run I linked earlier, https://github.com/risa-labs-inc/BossConsole/actions/runs/34826750597, ran on 03ba543b, which this rebase has rewritten. I am leaving the link because the content it validated is still exactly what is on the branch, but it is no longer reachable from the head and should not be read as covering it.

Marking this ready for review, which was the last of the steps you listed. The findings are addressed, the correction is posted above, and it is rechecked against latest dev as of 5a2e8181. The remaining item is the one I cannot do from here: this head is at action_required like every push of mine on this branch, so hosted CI needs your approval before it reports anything. The Windows dropdown visual check is still unperformed and I am not claiming it.

No merge or auto-merge is authorized.

@Vidhaankhare16
Vidhaankhare16 marked this pull request as ready for review September 14, 2026 12:36
@kshivang

Copy link
Copy Markdown
Contributor

@Vidhaankhare16 Your follow-up resolves the useful Claude findings: stored labels preserve their project suffix, empty path segments are filtered, trailing project separators retain the project name, and the added fallback/idempotency tests exercise meaningful cases. I independently reviewed current head 71b736da2872d124ddfdda04d409d8d6762360d6 and all 16 tests. No blocking code defect found and no agent source repairs were needed; implementation and revision credit remain yours.

Fresh local macOS validation at this head passed through the shared build lock: :composeApp:desktopTest --tests '*RunConfigurationNameSeparatorTest' (16 tests, zero failures/errors/skips), :composeApp:ktlintCheck, and :composeApp:detekt. The first attempt hit a sandbox Gradle cache permission error; the authorized retry completed successfully. Test home isolation was retained. The patch is still needed on current main/dev, and merge-tree against dev 600423381 is clean with only the two intended files changed. No newer identity fixes are displaced. #594/#595 command generation and #634 bridge identity are distinct work; host placement is appropriate.

I approved current-head CI run https://github.com/risa-labs-inc/BossConsole/actions/runs/34844262306 . Code quality, pgTAP, shell tests, version check and AI contracts passed; macOS/Linux/Windows builds and compatibility checks remain pending. This is infrastructure/validation still in progress, not an author defect. The older green run is not being treated as current-head coverage.

Converting to draft for the bounded review handoff, solely because these merge gates remain unresolved. No source changes are requested. Next steps: inspect the completed current-head CI results, investigate any actual failure with its job log, then mark ready and refresh dev/head/mergeability before a serialized exact-head squash merge into dev. Local reproduction command is above. The Windows dropdown visual check remains unperformed: open two same-named configurations in different directories, verify distinct parent/file labels and retained project suffix without the absolute path, and verify history labels after reload. No app was launched. The existing applicable Claude review and your responses were checked; no repeated Claude request was made.

@kshivang
kshivang marked this pull request as draft September 15, 2026 01:40
@Vidhaankhare16
Vidhaankhare16 marked this pull request as ready for review September 15, 2026 02:44
@Vidhaankhare16

Copy link
Copy Markdown
Contributor Author

Run https://github.com/risa-labs-inc/BossConsole/actions/runs/34844262306 has completed. All ten checks are green on 71b736da2872d124ddfdda04d409d8d6762360d6, the exact head you reviewed: build-test on ubuntu, macOS and windows, code-quality, compatibility-test, BOSS AI contracts, Shell Script Tests, Database (pgTAP), version-check and build-summary. Required current-head validation was the only gate you named, so I have marked this ready for review.

Rechecked against current dev, which has moved twice since your comment and is now d9c0cbbd. This branch still merges clean against it, with only the two intended files changed, DesktopRunConfigurationManager.kt and RunConfigurationNameSeparatorTest.kt. No push was needed and the head is untouched, so the approval you granted still covers exactly what is published.

Thank you for the independent macOS run at this head through the shared build lock. That is stronger evidence than my own Windows run, since it exercises the platform my change is not written for and confirms the separator handling is not Windows-specific in a way that would regress elsewhere.

One thing I am still not claiming: the Windows dropdown visual check remains unperformed. Two same-named configurations in different directories, distinct parent and file labels with the project suffix retained and no absolute path, and history labels after reload. It is the one part of this change I have only verified through tests.

No merge or auto-merge is authorized.

@kshivang
kshivang merged commit 7082e2c into risa-labs-inc:dev Sep 15, 2026
10 checks passed
@kshivang

Copy link
Copy Markdown
Contributor

@Vidhaankhare16 Merged into dev as 7082e2cbb2bec5a9478b60c78cf259a7387b635d. Thank you for the focused Windows fix and the follow-up preserving stored project brackets, filtering empty segments, and testing fallback/idempotency behavior. All implementation and test credit remains yours; this review added no code repairs.

Verified all ten CI checks succeeded on exact head 71b736da2872d124ddfdda04d409d8d6762360d6, and retained the prior independent locked macOS validation: 16 focused tests, ktlintCheck and detekt passed. The applicable Claude findings are addressed by your follow-up. Under the shared merge lock I refreshed main/dev and PR readiness, head and checks, verified integration changes only the two intended files, and used an exact-head squash merge into dev. No main merge.

The current integration did not already contain this fix. All-state PR/issue discovery and adjacent diffs showed command generation (#595/#594), runner scheduling (#486), path helpers (#422) and bridge security (#634/#695) are separate work, so none was consolidated or closed.

Manual check still outstanding: on Windows, open same-named run configurations in different directories, verify distinct parent/file labels with project suffixes and no absolute project path, and confirm history labels remain stable after reload. No app was launched during this review.

kshivang pushed a commit that referenced this pull request Sep 15, 2026
* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.
kshivang added a commit that referenced this pull request Sep 15, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Dow…
kshivang added a commit that referenced this pull request Sep 17, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachab…
kshivang added a commit that referenced this pull request Sep 17, 2026
)

* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Do…
kshivang added a commit that referenced this pull request Sep 17, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not r…
kshivang added a commit that referenced this pull request Sep 17, 2026
… a dismiss button (#154) (#742)

* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back …
kshivang added a commit that referenced this pull request Sep 17, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down…
kshivang added a commit that referenced this pull request Sep 17, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachab…
kshivang added a commit that referenced this pull request Sep 17, 2026
)

* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Do…
kshivang added a commit that referenced this pull request Sep 17, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not r…
kshivang added a commit that referenced this pull request Sep 17, 2026
… a dismiss button (#154) (#742)

* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back …
kshivang added a commit that referenced this pull request Sep 17, 2026
* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoticeDialog dismisses to acknowledge(), clearing the
  once-per-session notice and its restart offer unread on an alt-tab away.
- ScreenCapturePickerDialog dismisses to cancel the capture request, so
  switching to the window you want to share silently killed the share.

Add an internal LocalDismissModalOnFocusLoss CompositionLocal (default
true) that the two dialogs set to false, honoured in
shouldDismissOnFocusLoss. This suppresses only the focus-loss path -
Escape and the scrim still dismiss deliberately. Kept host-side rather
than as a BossDialog/modalRenderer parameter because those signatures are
pinned by the binary-compatibility validator and would need a coordinated
api-then-host release; both callers are host dialogs in composeApp.

Covered by HeavyweightOverlayTest.

* fix(overlays): address dismissal review and pin destructive dialog wiring

* style(overlays): order dialog property import

* style(overlays): wrap dismissal wiring assertion

* style(overlays): import the regression fixture root helper

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix toast overlay clipping content at its first-frame size (#154) (#354)

* Fix toast overlay clipping content at its first-frame size (#154)

HeavyweightCorner measured its content against a ceiling of
min(initialSize, region), so the toast overlay's 432x600 initialSize
doubled as a hard clip. Three verbose toasts can exceed 600dp, and
because the overlay window is content-sized the overflow is not
cosmetic: the bottom toast's dismiss button lands outside the window,
unclickable, on the INDEFINITE path where dismissing is the only way
out, so the toast is genuinely stuck.

measuredAgainst already decoupled measurement from the window's current
size, so the two uses of the initial size no longer need to be one
number. Size the ceiling to the parent region instead (regionCeiling),
leaving initialSize as only the small first-frame placeholder. Content
now grows the window up to what the parent can actually show rather than
being clipped. Other callers are unaffected: intrinsically-sized content
measures the same, it just stops being capped below the parent.

Covered by HeavyweightCornerTest; HeavyweightCornerSizingTest's ratchet
guarantees are unchanged.

* test(overlays): measure content beyond the initial toast window height

* docs(overlays): update all first-frame sizing contracts

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143) (#367)

* Overlay follow-ups: popup measure guards, anchor diagnostic, dialog a11y (#143)

Equality guards on BossPopup's measuredWidthPx/anchorPositionPx make onGloballyPositioned/layout writes no-ops in the steady state instead of a redundant compose-measure-layout pass per frame during a window drag. AnchorBounds popups that never measure report once via BossOverlayHost.reportUnmeasuredAnchor() after a grace period. The modal card declares semantics { dialog() } and swallows clicks with detectTapGestures instead of clickable, so a screen reader announces a dialog rather than a button; the scrim dismiss also moves to detectTapGestures so IsDialog is not merged under a clickable ancestor (which crashed the modal). BossAlertCard drops the header spacer for a buttons-only card (alertHeaderSpacerVisible); AlertWidth renamed ALERT_WIDTH. Covered by BossAlertCardSpacerTest, ScrimmedModalSemanticsTest, and the existing ModalInputArmingTest/BossAlertCardLayoutTest.

* fix(overlays): refresh scrim dismissal callback after recomposition

* fix(overlays): remove redundant measurement reads and cover card taps

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-ui-core): guard write-once overlay registry fields (#369)

* fix(plugin-ui-core): stop redundant popup recomposition and guard write-once overlay registry fields

Two non-blocking follow-ups from the HARDWARE dialog sweep (#143).

BossDialog wrote measuredWidthPx and anchorPositionPx unconditionally
from layout {} / onGloballyPositioned, and both are read during
composition - so an unchanged value still invalidated composition from
inside layout on every pass, costing a redundant composition-measure-
layout cycle per frame for every open BossPopup during e.g. a window-
resize drag. Both writes are now equality-guarded, a no-op once the
value stops moving.

BossOverlayHost.{useHeavyweightOverlays,modalRenderer,popupRenderer,
diagnostics} are public mutable statics the KDoc calls host-owned and
write-once, on a singleton shared across the host and every in-process
plugin - but nothing enforced it, so one plugin line could reinstate the
occluded-dialog bug this file exists to fix. Each now locks to its first
write (via a custom setter, so the JVM descriptor - and so binary
compatibility with the boss-plugin-api mirror - is unchanged) and reports
a duplicate write through diagnostics instead of silently accepting it.
openHeavyweightPopups is deliberately left alone: its own KDoc documents
it as a running counter across a popup's lifetime, not a one-time
registration, and a write-once guard would freeze it at whatever the
first popup left it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(overlays): isolate write-once registry fixtures and remove duplicate popup edits

* fix(overlays): register diagnostics first and verify both startup modes

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-api-core): guard against host/api divergence in duplicated packages (#368)

* fix(plugin-api-core): guard against host/api divergence in duplicated packages

ai.rever.boss.plugin.{logging,bookmark,workspace,browser,scrollbar,ui,
tab.terminal} each exist twice: once in the host modules, once inside
boss-plugin-api, what plugins actually compile against. The host's copy
shadows the api's parent-first inside plugin classloaders, so a plugin
whose bytecode references a member the api has and the host lacks fails
to link at runtime rather than at compile time - already the cause of
two incidents (secret-manager 1.2.6/1.2.7, unloadable on every host with
only "ComponentLogger.$stable: field not found" as a clue).

LoggingStableFieldTest, BookmarkStableFieldTest and WorkspaceStableFieldTest
each pin the one field that has actually bitten us. Nothing pinned the
general case: an added method or a changed signature in either copy
breaks a plugin the same way and would be diagnosed from scratch.

ApiPackageDivergenceTest diffs the host's copies against the pinned
release jar plugin-api-core's own build already downloads
(fetchApiPluginJar), for every public, non-synthetic member of the seven
packages actually duplicated (verified against the real jar's contents,
not guessed from the issue's approximate count) - not ai.rever.boss.plugin.api
itself, which is filtered from that same jar so it cannot diverge from
itself, and not ai.rever.boss.plugin.bundled, which is api-only.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(plugin-api): cover callable ABI and preserve PanelConfig linkage

* fix(tests): compile legacy serialization bridge and satisfy lint

* test(plugin-api): address review and verify loader isolation

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(gate): surface remedy success and handle in-flight update states (#378)

* fix(gate): handle updateHost success and in-flight download states

* style(gate): resolve detekt LongMethod and ReturnCount violations

- Extracted PluginLoadGateHeader to reduce PluginLoadGateBody length under 60 lines.

- Refactored updateHost to use a single return expression, eliminating multiple exit points and fixing line length limits.

* fix(gate): complete header extraction and validate update remedy state

* fix(gate): retain rollback and report installation progress accurately

* fix(gate): explain the install step after downloading BOSS

* style(gate): apply ktlint and wrap detekt overlong lines

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Make MCP search rows open Toolbox for kill-switches (#380) (#382)

* Make MCP search rows open Toolbox for kill-switches (#380)

Agent-less operators could find mcp__boss__* tools in double-shift search
but selecting a row did nothing. Wire selection to reveal Toolbox
(plugin-manager) and document the minimum attach / kill-switch path.

* fix(search): validate MCP navigation and clarify Toolbox hint

* fix(search): report unavailable Toolbox for MCP selections

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* # Governed Autonomy for MCP tools: approval gate and audit ledger (#371)

* feat(mcp): implement Governed Autonomy with Operation Ledger and ASK mode approval gate

- Add McpPolicy and McpPolicyEngine with fail-closed configuration and session trust
- Add McpApprovalBus and McpApprovalDialog for non-blocking interactive tool approval
- Add McpOperationLedger with size-based file rotation (10 MB x 5) and sensitive argument masking via LogSanitizer
- Integrate policy checks, coroutine approval gate, and ledger journaling into McpToolRegistryCore.invoke
- Add UI wiring in BossAppState, BossAppEventBusEffects, BossAppDialogs, and BossBottomBar
- Add comprehensive unit tests covering policy, approval, ledger rotation, and registry invoke integration

* fix(mcp): address review findings across approval gate, policy engine, and ledger

* fix(mcp): avoid length-based over-redaction of tool arguments

McpOperationLedger and McpApprovalDialog sanitized arguments through
LogSanitizer.sanitizeMap, which masks any string value 20+ characters
long regardless of content. That defeated the stated goal of keeping
long file paths, URLs, and shell commands readable in the audit log
and, worse, in the approval dialog an operator relies on to decide
whether to approve a mutating call.

McpArgumentSanitizer replaces that path for both call sites: a value
is now only masked when its key names it as sensitive, or its shape is
unambiguously a credential (JWT, GitHub token, sk_/pk_ vendor key).
Adds a test proving shape-based masking still catches a credential
under a non-sensitive key name, and renames the existing redaction
test to match.

Also reorders two imports in BossAppDialogs.kt that were out of
alphabetical order and would fail ktlintCheck.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix(mcp): repair governed dispatch and cancellation audit guarantees

* fix(mcp): address governed autonomy review defects

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop the Shortcuts tester reporting working shortcuts as broken (#375) (#376)

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down…
kshivang added a commit that referenced this pull request Sep 18, 2026
…ence (#876)

* test(browser): run the form field detector against a fake DOM (#609)

* test(browser): run the form field detector against a fake DOM

FormFieldDetector is JavaScript living inside a Kotlin string, and it is the
only one of those with no Kotlin test at all - so nothing read it, by grep or
by execution, while it was injected into every frame of every page from
BrowserHandleImpl.injectPageHelpers.

This executes both of its scripts against a fake DOM covering only what they
touch, so an added DOM read fails loudly. It follows test-browser-collector.js,
which exists for the same reason and whose own note records that every budget
bug in the collector so far was found by running it rather than reading it.

It also closes a coupling the Kotlin side cannot see. The injected script emits
nine keys and parseFieldInfoJson picks them back out with one hand-written
regex per key, across two files and two languages. A rename on either side does
not fail, it silently yields an empty string for that field forever. Both key
sets are read from source here and compared.

Four checks pin defects rather than desired behaviour. Each is marked and names
its follow-up, and they are pinned rather than fixed so the fix lands as a
reviewable diff against a measured baseline:

- The script has no re-injection guard. A second injection into one document
  leaves two listener pairs, and the host re-runs injectPageHelpers on every
  main-frame NavigationFinished, which for a single-page app is a route change
  within one document. BrowserInteractionScript guards exactly this case, and
  the harness reads that file to confirm the omission is not house style.
- Every focus is logged to the page's own console with the field's name or id.
- The focusout handler dereferences document.activeElement unguarded. That is
  nullable per spec, and the throw leaves the blurred field still referenced.
- parseFieldInfoJson matches "key":"([^"]*)" against JSON.stringify output, so
  a value containing a double quote is truncated and keeps a trailing
  backslash. The pattern is reconstructed from the Kotlin source rather than
  restated, so it cannot drift.

Sources are read line-ending normalised, since a Windows checkout with
core.autocrlf on has CRLF.

* test(browser): model textarea type and normal focus transitions

* test(browser): preserve independent timer failures and escaped values

* test(browser): assert normal blur timers complete without errors

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(updater): reject overlapping host plugin updates (#612)

* fix(updater): reject overlapping host plugin updates

* docs(updater): clarify admission scope and shared progress notice

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(run): preserve concurrent configuration updates (#755)

* fix(plugins): let a plugin's search provider reach global search (#670)

* fix(plugins): let a plugin's search provider reach global search

A dynamic plugin's register() receives a TrackingPluginContext around a
SandboxedPluginContext around the window's DefaultPlugin, and only
DefaultPlugin put a SearchProvider into SearchRegistryImpl. Neither
wrapper overrode registerSearchProvider or unregisterSearchProvider, so
the call reached the PluginContext default, whose body does nothing. The
bookmarks plugin registers its BookmarkSearchProvider this way, and
GlobalSearchService gets bookmark results only from that registry.

TrackingPluginContext now forwards both calls and records the teardown
with recordUiExtensionRegistration, like panel menus, settings pages,
deep-link handlers, shortcuts and status bar items, so unregisterAll
removes the provider when the plugin is disabled or unloaded.
SandboxedPluginContext forwards both calls like the registries above.

PluginContextWrapperForwardingTest checks that both wrappers declare
every PluginContext member. It uses Kotlin reflection because the api is
compiled with -jvm-default=enable, which adds a compatibility bridge for
each default method to implementing classes, so Java reflection reports
members a wrapper never overrode.

* fix(plugins): forward organisationMembersProvider and check inherited members

Rebased onto dev, which moved to plugin API 1.0.93 (#693). That api adds
PluginContext.organisationMembersProvider, which neither wrapper declared,
so PluginContextWrapperForwardingTest failed on the rebased branch for both
of them. Both wrappers now forward it. DefaultPlugin does not implement it
yet, so every plugin still gets null; the forwarding only keeps a future
host implementation from being swallowed by the wrappers.

From review:
- The guard now expects every member of PluginContext including inherited
  ones (less Any's), so splitting the api interface cannot quietly narrow
  it. A test with a small interface hierarchy pins that. Its KDoc states
  what it cannot see: members match by name, and a new wrapper has to be
  added to it.
- The TrackingPluginContext comments that list the recorded kinds, and the
  unregisterAll KDoc, now name search providers.
- AGENTS.md records search providers as the second three-wrapper gap.
- PluginRegistrationsAcrossWindowsTest (from #694) left search out of its
  teardown-chain test because the wrappers did not forward it; it now
  covers all seven kinds.

* test(fluck): cover zoom and favicon preservation across tab copies (#777)

* fix(fluck): preserve custom zoom level and favicon cache key across tab copy and split view

* style: fix ktlint trailing blank line format in FluckTabInfoTest

* fix(auth): read a pasted magic link the way the redirect function does (#740)

"Paste magic link manually" rebuilt a boss://auth/verify link only when the
pasted text contained a literal "token=". The sign-in email's link wraps the
confirmation URL in the redirect function's url= parameter, and a copied link
can carry it percent-encoded, so the paste was passed on as a web address,
dropped as unrouted, and the screen showed nothing.

Reproduced on BOSS 9.5.17 with a real email: the encoded link did nothing,
and the same token with only url= decoded signed in.

signInDeepLinkFor decodes the query and reads token or token_hash from the
link or its url= value, with the type beside the token, else the outer
link's, else magiclink, matching supabase/functions/redirect/app.ts. It
refuses a token or type with characters that could add parameters to the
rebuilt link, and a token on an address that is not a verify or redirect
link. Text that is not a sign-in link now shows an error instead of nothing.

* fix(tabs): keep tooltip visible while title updates (#752)

* fix(home): add persistent navigation in the active pane (#757)

* fix(home): offer direct Home entry from New Tab

* feat(home): add persistent pane-local Home navigation

* test(home): account for permanent Home in rail resize budget

* fix(home): remove redundant Home creation from New Tab

* fix(home): use subtle navigation selection instead of a filled strip

* fix(home): highlight navigation content without full-row hover fill

* fix(security): restrict plugin_ratings to the server-side rating pipeline (#761)

plugin_ratings' four RLS policies (20260130000000) carry no TO clause, so
they apply to anon and authenticated:

- SELECT USING (true) let any holder of the shipped anon key read every
  rating row across all users - auth.users UUID, free-text review, and
  timestamps, unscoped by plugin visibility, the same cross-user leak
  class closed for plugin_downloads by 20260910120000 (#487).
- The write policies only bind user_id to the caller, so a
  self-registered user can insert a rating for an arbitrary plugin UUID,
  using the FK violation as an existence oracle for org-scoped plugins.

The desktop client never touches this table directly: the plugin-store
Edge Function runs with the service role and routes/browse.ts reads
aggregates through the SECURITY DEFINER stats functions, so restricting
client access changes nothing for the shipped app.

Mirror the downloads migration: REVOKE ALL from PUBLIC/anon/
authenticated, GRANT the four verbs to service_role, drop the permissive
policies, and re-state the intent as explicit TO service_role policies.
pgTAP coverage asserts no non-service-role policy remains (unscoped
included), no per-verb client table privilege, and that service_role
keeps the live path.

* fix(ui): split the editor breadcrumb on path segments so it works on Windows (#753)

The bottom bar built the breadcrumb by appending "/" to the project path,
stripping that prefix and splitting on "/" alone. On Windows both the project
path and the tab's file path are native (C:\Users\dev\repo), so the prefix
never matched, the split found no separator, and the whole absolute path
rendered as a single segment.

Move the logic into editorBreadcrumbSegments, which treats both "/" and "\\"
as separators - the display-label rule plugin-path-utils documents and
extractFileName already follows - and compares whole segments, so a project at
/repo does not claim /repo2/App.kt. The helper lives in its own file to keep
BossBottomBar.kt under detekt's TooManyFunctions threshold.

Adds EditorBreadcrumbSegmentsTest: 9 cases, none host-dependent.

* fix(performance): read plugin process memory and threads per platform (#785)

The Performance panel read memory and thread counts for out-of-process
plugin JVMs with `ps -o pid=,rss=` and `ps -M` on every platform. `ps -M`
is the macOS thread listing. Windows has no `ps` (Git for Windows' MSYS
`ps` rejects both flags), so every plugin showed 0 bytes and 0 threads.
On Linux, procps reads `-M` as a security-label column and prints one
line per process, so every plugin showed exactly 1 thread.

Move the query into PluginProcessMetrics:
- Linux: VmRSS and Threads from /proc/<pid>/status, no subprocess.
- macOS: the same two ps commands and counting rule as before.
- Windows: Win32 through JNA, already a desktop dependency. Threads
  from a Toolhelp process snapshot, memory from GetProcessMemoryInfo.
  Not PowerShell: this runs every 5 s from the snapshot collector.

Rewrite the ps -M counting loop as a pipeline with the same rule, and
drop the two detekt baseline entries that named the deleted code.

Adds PluginProcessMetricsTest: 10 cases, 9 of them host-independent.

* fix(search): shift Spotlight file-name highlights by the platform separator (#716)

searchFiles matches the query against IndexedFile.relativePath, then
rebases the resulting ranges onto the file name, because FileResult's
matchRanges index the name. It found the name's start with
lastIndexOf('/'), but relativePath is derived from two absolute paths
and so is joined with a backslash on Windows. The search returned -1,
the ranges were shifted by nothing, and the dialog underlined whatever
characters happened to sit at those offsets: `uiButton` against
`app\ui\Button.kt` underlined "on" and "kt" rather than "Button", and a
match landing past the name's length was dropped and underlined nothing.

Look for the separator alongside '/', not instead of it. On a POSIX host
the two are the same character, so that host is unchanged; on Windows '/'
is not a legal file-name character and cannot split a name by accident. A
bare lastIndexOf('\') on every platform would be wrong, since a backslash
is a legal POSIX file-name character. ContentSearchService in this
package already reaches for File.separatorChar for this reason.

The separator is a parameter defaulting to the host's. Because the rule
is deliberately platform-dependent, a Windows-shaped path proves nothing
on a POSIX host - the shift there is correctly zero - so injecting it is
what lets the Windows case be asserted on every runner rather than only
on a Windows one. The four shift cases do that; the end-to-end case uses
the host separator and is red only on Windows, which its KDoc states.

* feat(plugins): add a copy button to plugin toasts (#745)

* fix(run): make run-configuration disambiguation work on Windows (#653)

* fix(run): make run-configuration disambiguation work on Windows

makeNamesUnique and makeStoredNamesUnique split RunConfiguration.filePath
on a literal "/", but a stored filePath is an OS-native absolute path:
DesktopMainFunctionDetector assigns file.absolutePath, which is
backslash-separated on Windows. split("/") yields one part there, the
parts.size >= 2 guard never passes, and two configurations with the same
name in different directories stay byte-identical in the Run dropdown.

Split on both separators, and take the project name from extractFileName()
instead of substringAfterLast('/'). plugin-path-utils documents itself as
the single source of truth for path utilities, and
RunConfiguration.toShortNameWithProject already builds the initial label
with it; this file was the one place re-deriving those values with a
hardcoded separator.

The project-name change also closes a latent leak: substringAfterLast('/')
returns the whole absolute path on Windows, so the label would have read
"main (app/Main.kt [C:\Users\<name>\myproject])" once the split was fixed.

Both functions become internal so the tests can drive them directly.

* fix(run): keep the project bracket and drop empty path segments

Addresses the review on 03ba543b.

makeStoredNamesUnique replaced the whole trailing "(...)" group, so a stored
name of "main (Main.kt [myproject])" became "main (app/Main.kt)" and lost the
project bracket that makeNamesUnique rebuilds. That asymmetry is pre-existing
on POSIX, but the branch never executed on Windows before this change, so the
fix above is what makes it reachable there. loadSettingsSync assigns the
rewritten names to _currentSettings and the next saveSettings persists them,
so the loss is not display-only. The bracket is now carried across.

Both splits drop empty segments, matching DesktopMainFunctionDetector
.detectModuleName. A doubled separator can reach these functions through a
hand-edited run-configurations.json, and without the filter takeLast(2) picks
up the empty segment and labels it "/Main.kt". The filter subsumes the leading
separator trim, which is removed rather than left as dead belt-and-braces.

The project name is now read from projectPath.trimEnd('/', '\'), so a project
path with a trailing separator still yields a bracket instead of dropping it.

Both regexes move to object-level vals: the trailing-group pattern was
duplicated across the two functions, and neither needs recompiling per element.

Tests go from 10 to 16, adding bracketed stored names on both platforms, a
doubled separator in each function, idempotency of the stored rewrite, and a
projectPath that is not an exact prefix of filePath.

* Gate JxBrowser auto-release on the compile-classpath modules (#678)

* Gate JxBrowser auto-release on the compile-classpath modules

The autorelease watcher's artifact-readiness probe only checked the core
jxbrowser jar and the platform binaries. But the desktop compile classpath
also needs jxbrowser-compose and jxbrowser-swing (plus jxbrowser-kotlin,
transitive of compose), each published as its own Maven module whose
propagation can lag the binaries.

When JxBrowser 9.5.1 landed, the binaries were resolvable but those modules
were not, so the gate passed, a build was dispatched, and every branding
matrix job failed at desktopCompileClasspath with:

  Could not find com.teamdev.jxbrowser:jxbrowser-compose:9.5.1

Add the compile-classpath modules to the probe so a version is not dispatched
until every artifact the branding build actually resolves is available.

Fixes #667

* test: cover missing JxBrowser compile modules in release gate

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): report workspace health in boss status and add boss doctor (#579)

* feat(cli): report workspace health in boss status and add boss doctor

boss status --json gains an additive health object built from state BOSS
already keeps: sandbox watchdog disables and plugin health rows from #454,
a browser engine that is missing, failed or unresponsive, and MCP
kill-switch and policy faults. boss doctor prints the same report, one line
per finding with a suggested next step, and exits 2 while anything is
reported. Nothing is changed by either command.

doctor is registered in createBossCLI, the headless gate in
CliBootstrap.isHeadlessCli, OsOpenArguments.CLI_SUBCOMMANDS and the three
launchers. OsOpenArgumentsTest now checks CLI_SUBCOMMANDS against
createBossCLI().registeredSubcommands(), and CliBootstrapTest pins doctor
as a headless command.

Refs #418

* fix(cli): keep health inspection read-only and qualify unchecked reports

* fix(cli): contain each window's plugin health source and report partial coverage

WorkspaceHealthSources.pluginSnapshots() mapped every registered window in
one expression, so one source that threw propagated out and the collector's
per-area try turned the whole plugins area into unchecked. A watchdog-stopped
plugin in window A disappeared whenever window B's source failed, which is
exactly the case the feature exists to report.

Each window source is now contained on its own. Every snapshot that could be
read is kept, failures are counted, and each failure is logged through
BossLogger with LogCategory.SYSTEM the way the collector already logs an
unreadable area.

unchecked still means nothing could be read at all. A new additive partial
set says an area was read from several sources and some of them failed, so
the findings listed are real but do not cover the area. An area is never in
both. boss doctor prints a "Partially checked:" line and stops saying "No
problems found."; boss status appends "partially checked: plugins" to its
Health line. Today only plugins can be partial, because it is the only area
with one source per window.

partial is additive on the wire: a reader that does not know the field sees
what it saw before, and both CLI commands read an absent partial as empty,
so an older CLI against a newer BOSS and the reverse both still parse.

WorkspaceHealthSourcesTest now registers a failing source beside one that
reports a stopped plugin and asserts through the real collector that the
stopped plugin is still reported and the area is declared partial.

Refs #418

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(cli): write piped CLI output as UTF-8 (#604)

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(cli): add plugin init, validate, and link commands with dev hot-reload (#468)

* test(panels): retain sibling lifecycle isolation coverage from #313

Adapt the original #313 test to the #317 constructor and supply the missing panelInfo overrides. Original test scenario and assertions are by @rehannayeem0786; these compile adaptations are maintainer integration work.

* fix(panels): complete lifecycle cleanup across failure and window teardown

Destroy partially constructed panels and replacements that fail to resume. Continue downward lifecycle transitions after a failing pause or stop callback. Dispose panels before plugin teardown in the same callback rather than relying on sibling Compose effect order. Add regression coverage and adapt the retained #313 test bodies for detekt. These changes are maintainer follow-up to the original #313/#317 contributions.

* docs(ui): remove stale disabled top-bar feature list

* test(ipc): protect advisory menu item ID uniqueness

* fix(panels): keep store registration scoped to its window effect

Remove the redundant unregister from plugin teardown. Document independent context services and synchronous Essenty onCreate replay, with tests for registration ownership and exactly-once create callbacks. Keep tab teardown hardening separate. Maintainer follow-up to the review of #317; original #313/#317 contributor credit is unchanged.

* fix(crash): address consolidated sanitization review notes

Maintainer follow-up to Claude N1-N4: document factory/copy invariant, correct stale comments, move Aditya original construction assertions into the service suite while retaining Antriksh blank/diagnostic cases, and log unexpected dialog submission failures. Original contribution credit remains separate.

* fix(logging): keep server failure payloads out of secret RPC logs

Maintainer follow-up to Claude review: log operation and exception type only, preserve sanitized return semantics, and check each catch for safe return/log wiring. Document measured hostname limits without broadening the matcher; add module-local redaction and diagnostic-preservation coverage. Antriksh original implementation remains credited separately.

* ci: build PR batches on dev

* Allow Claude diff reviews after approved fork builds (#413)

* fix(browser): preserve newer clipboard copies during plain-text paste (#316)

* fix(browser): paste-without-formatting no longer clobbers a copy made in its restore window (#205)

* fix(browser): make paste-without-formatting's restore identity-based, not text-based

Review of this PR caught that the text-equality guard cannot distinguish 'our write is still current' from 'a previous press already restored the rich original' - whose string projection is by construction the same text - so two presses inside the 200ms window permanently downgraded a rich clipboard to plain text. A PasteWithoutFormattingSession now tracks the exact Transferable each press installed and restores the pre-window original once, while a user copy in the window still wins. Adds five tests to BrowserClipboardCommandsTest, including the double-press regression; wraps the two over-limit lines; e.message -> e::class.simpleName in the touched handlers.

* fix(browser): fold tryRestore's early exits so it stays within detekt's ReturnCount

* fix(browser): blank lines between FakeTransferable overrides per ktlint

* test(browser): retain clipboard restore scenarios from #408

* fix(browser): track clipboard ownership through AWT wrappers

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* fix(plugins): serialize concurrent first loads per class name (#324)

* fix(plugins): prevent duplicate class definitions during concurrent loads

* fix(plugins): make class loading parallel-safe

* test(plugins): pin concurrent loading lifecycle boundaries

Maintainer consolidation follow-up: retain #324 synchronization and its tests; add concurrent shared-name, superclass, and unload-marker coverage. Document caller-sensitive registration based on arjun28115 review feedback and clarify that marking unload does not drain an already admitted parent lookup. No global lifecycle lock or resource-policy changes.

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Procs <168113425+ProportanilityConstant@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(browser): defer native disposal until admitted calls drain (#312 + #409) (#412)

* fix: make BrowserHandle JavaScript execution cancellable

the previous synchronous JxBrowser call blocked the Main/EDT thread;
coroutine cancellation could not take effect while that call was blocking;
the implementation now uses JxBrowser’s asynchronous JavaScript callback API;
suspendCancellableCoroutine makes the Kotlin waiting side cancellable;
late callbacks after cancellation are safely ignored;
this does NOT terminate JavaScript already executing inside Chromium.

* fix: add robust native call lifecycle tracking for disposal

* test: verify native operation cleanup on synchronous failure

* fix: linearize browser disposal and pending operation tracking

* fix: resolve detekt violations in browser native tracking

* test: satisfy detekt failure simulation rule

* fix: close native operation disposal race

* fix(browser): drain an in-flight call before closing the browser

Issue #300 reports that executeJavaScript is a synchronous, non-
cancellation-aware native call, and a plugin's own withTimeoutOrNull
around it can only abandon the caller's wait - not the call itself -
which stays running on Main indefinitely and can race a concurrent
handle.dispose(), touching the same native object from two threads at
once.

The severe half of this was already fixed by BoundedBrowserCall: this
class confines every blocking round trip to one dedicated daemon thread
instead of Dispatchers.Main, so a wedged renderer no longer freezes the
app. What was not yet closed is the residual window this issue's core
report is actually about: shutdown() stopped new work but did not wait
for whatever was already running, so a caller could proceed straight to
browser.close() while a call from just before shutdown was still
finishing on the dedicated thread.

shutdown() now waits, bounded, for that in-flight call to drain before
returning. Costs nothing in the common case - awaitTermination returns
immediately once the one worker thread and its queue are both idle,
which is where an instance sits between calls - and only the bound in
the rare case something was genuinely still running.

Known, and disclosed in the KDoc rather than claimed away: this narrows
the race for the common case (a call that was always going to finish
quickly) rather than closing it. It cannot help when the in-flight call
is itself the wedge this class's own deadline exists for - waiting
unboundedly for a genuinely stuck call would reintroduce the exact freeze
BoundedBrowserCall exists to prevent, just moved from a plugin's await
into every caller's teardown path. There is no JxBrowser API on this
version able to interrupt a blocking round trip already inside the
native call, which is the whole reason the call is confined to its own
thread instead of cancelled - closing that window completely needs
JxBrowser's own cooperation, which the issue's own analysis already
concluded is unavailable.

Tests: shutdown waits for a fast in-flight call to finish before
returning, and does not wait past its own drain timeout for a wedged one
- the same before/after pair the rest of this test file already uses for
BoundedBrowserCall's deadline.

* fix(browser): defer native close until admitted calls drain

Consolidates the lifecycle work from #312 (johncybersage) and #409 (Antriksh1984), retaining both original histories. Replaces success-only callback accounting and timeout-then-close with executor termination signals and host-owned native/profile cleanup. These corrections and replacement tests are maintainer/agent work, separate from contributor scoring.

* fix(browser): bound profile waits and clarify deferred cleanup ownership

Address consolidation review: schedule both service disposal paths without awaiting native close, diagnose pending drains, centralize executor ownership, and bound profile lease acquisition without permitting unsafe reuse or eviction. Document process-exit abandonment and verify wiring and cleanup ordering. Maintainer/agent follow-up, separate from original #312 and #409 contributor credit.

* fix(browser): schedule disposal after early UI teardown failure

* ci: create launcher directory before Claude native installation

* Revert "ci: create launcher directory before Claude native installation"

This reverts commit 5184908e6c3f133502508513b4c5e17651b13bc0.

* test: keep warmup registry ordering check on one event loop

* test(browser): normalize CRLF in disposal source wiring guards

---------

Co-authored-by: john k <johnk@johns-MacBook-Pro.local>
Co-authored-by: john k <johnk@syn-172-100-137-112.res.spectrum.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>

* feat(tabs): show a speaker glyph on tabs that are playing audio (#314)

* feat(tabs): show a speaker glyph on tabs that are playing audio (#308)

* fix(tabs): review round - compile, registry lifetime, UI-thread marshalling, owner flush

1. graphicsLayer import dropped (does not exist at that package); the fade now uses .alpha(audioAlpha), already imported. 2. TabAudioRegistry no longer outlives its owner: disposeAllTabsBlocking unregisters everything this component registered, and DetachedTab.destroy drops the entry for a tab destroyed without adoption - the handler captures the BossTabsComponent, so stale entries retained it. 3. The registry update is marshalled to the EDT via SwingUtilities.invokeLater, so the tab-model mutation happens on the UI thread as the KDoc claims. 4. setFullscreenHandler flushes the current playback state once ownerTabId becomes known, closing the starts-before-registration race the review could not verify. 5. TabAudioRegistryTest added: delivery, last-writer-wins, ownership-checked unregister.

* fix(tabs): CI round 2 - DetachedTab receiver error, long line, and baseline-preserving signatures

- BossMainWindowPanel.kt:2098: DetachedTab is a non-inner nested class and cannot use the outer component as receiver; the audio-handler drop moves to detachTab instead, which is where adoption-vs-destroy actually diverges. - TabAudioRegistry.kt:9 wrapped under detekt's 120-char limit. - FluckTabInfo.isPlayingAudio and BossTabButton's glyph read no longer change any signature: the flag is a mutable property (copied in copy()'s body) and the button reads it off tabInfo, so the constructor, copy() and BossTabButton keep their exact baseline-frozen detekt IDs and the resurfaced LongMethod/LongParameterList/CyclomaticComplexMethod findings retire.

* fix(tabs): fold handleAudioUpdate within ReturnCount; add missing final newline in the test file

* fix(tabs): publish audio state by browser ownership and keep icon layout stable

---------

Co-authored-by: rehannayeem0786 <rehannayeem0786@github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: connect chrome density settings and keyboard controls (#305)

* feat: wire the chrome density scale into settings and small-screen defaults

ChromeDensity / ChromeDimens.of() / LocalChromeDimens already existed on
main and every bar (BossTitleBar, BossTopBar, the main tab bar,
BossBottomBar) already read its height from BossChrome.dimens - but
LocalChromeDimens was never provided anywhere, so it silently resolved to
its staticCompositionLocalOf default (Comfortable) always. The scale was
built and unreachable.

This closes that gap:

- WindowAppearanceSettings gets a `density` field (default COMFORTABLE, so
  an existing install's settings file - which has never heard of this key
  - decodes to exactly the chrome it already had; no settings-version bump
  needed).
- BossApp.kt now provides LocalChromeDimens from that field, host-only and
  outside BossAppCompositionLocals (plugins never draw host chrome).
- A "Density" dropdown in Settings > Bars (Compact/Comfortable/Spacious),
  indexed in Settings search.
- A fresh install on a small screen (< 1000dp logical height, clearing the
  13" MacBook Air's ~931-956pt from #239's own measurements) now defaults
  to Compact and starts with the bottom bar off - the one bar this manager
  can still reclaim itself, since the side strips are already off by
  class default. Toolkit.getScreenSize() is read once, wrapped in
  runCatching (HeadlessException off a display must not break a fresh
  install), with a pure defaultDensityFor(screenHeightDp) so the decision
  is unit-testable without a display.

Addresses #239, scoped down from the full issue. Out of scope in this
pass, deliberately:

- The app does not yet report its own chrome budget as a measured
  percentage - the issue's own "state it, don't argue it" ask. Reachable
  later from the same ChromeDimens source, but needs a spot to render it.
- The title bar's 27dp "Boss Console" label row is untouched - collapsing
  or merging it with the tab bar is a separate, riskier layout change than
  a density scale.
- No regression intended to the existing per-bar show/hide switches or
  focus-mode edges: density only changes how much room a bar that is
  already on screen takes, never whether it is shown.

* fix: drop the Compact-density bottom-bar auto-off, wrap an overlong line

Address review feedback on #305:

- Removed showBottomBar = density != ChromeDensity.COMPACT from
  getDefaultSettings(). ChromeMetricsTest already shows the shipped
  macOS defaults reach 93.3% of a 931dp window at Comfortable, so
  #239's >=90% bar is met without touching the status bar - and that
  bar's own KDoc argues it must stay on ("the only always-on readout").
  It was also reachable from the settings-load catch fallback, so a
  transient read error could silently drop the bar and have the next
  save persist the loss. Compact now only changes density, nothing else.

- Wrapped primaryScreenHeightDp() - was 121 chars, one over detekt's
  default MaxLineLength (120), which applies here since .editorconfig's
  140 is a ktlint-only setting and this line wasn't in the baseline.

* feat: add chrome density setting

* feat: add small-screen chrome defaults

* feat: add chrome density controls and small-screen defaults

* fix(ui): preserve lean density defaults and test density controls

* style: format multiline screen-height lookup

* fix(ui): separate density recovery and make editor chords opt-in

* style: wrap density default documentation

---------

Co-authored-by: AdityaK-iiita <adityakolate0070@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): stop host resource fallback after unload (#325)

* fix(plugins): stop host resource fallback after unload

* fix(plugins): preserve resource warning for a missing result

---------

Co-authored-by: sgoel2be24-cyber <223222024+sgoel2be24-cyber@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Handle closed browser frames during navigation callbacks (#400)

* Handle closed browser frames during navigation callbacks

* test(browser): retain closed-event guard after navigation integration

* fix(browser): preserve helpers on PID failure and recognize closed transports

* style(browser): wrap review diagnostics for detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* refactor(window): pin the panel content alignment width (#398)

* fix(window): restore strict width constraints for nested panels

Fixes #278. Commit c15660c5 inadvertently dropped the explicit width constraint for nested panels in the right dock by wrapping them inside a PanelColumn with a Box(weight(1f)) that lacked fillMaxWidth(). This loose constraint caused a known issue with SubcomposeLayout caching stale bounds when resized, resulting in Row content disappearing after weighted Spacers. This commit restores the .fillMaxWidth() propagation.

* Cover panel content alignment across width changes

* Keep panel layout regression in the existing app test package

* Document panel alignment contract and pin filling content width

* Wrap the panel width assertion to satisfy detekt

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* #320 - Fix quick actions vanishing in short tab rail (#328)

* #320 - Fix quick actions vanishing in short tab rail

* Fix: detekt & ktlint checks

* Fix rail action budgets and reversible fallback wiring

* Fix resize test assertion import and update rail coverage note

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(crash): refine scroll boundary and bound error sanitization (#350)

* fix(crash): refine scroll boundary and bound error sanitization

* Avoid exposing a partial token at the crash message limit

* Document sanitizer input bound and verify expansion ordering

---------

Co-authored-by: Aishwary Anand <aishwary.cd22@bmsce.ac.in>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(logging): redact private hostnames (#346)

* fix(logging): redact private hostnames

* Handle private hostname punctuation and preserve diagnostic ports

* Format hostname punctuation regression assertion

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat: Opening an .html file should ask open as a file or webpage (#333)

* feat: Opening an .html file should ask open as a file or webpage

* Fix HTML prompt delivery and persisted preference ordering

* Cover HTML routing and correct review test fixtures

* Honor HTML navigation intent and release pending prompts on failure

* Isolate HTML prompt handling from the general dialog host

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugin-store): parse timestamps consistently in list and detail metadata (#379)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* fix(repository): implement robust ISO timestamp parsing for Supabase payloads

* style(repository): resolve detekt MaxLineLength and TooGenericExceptionCaught

* fix(plugin-store): implement plugin metadata timestamp parsing

PluginStoreClient.parseTimestamp was a stub that always returned 0L, so
every plugin fetched from the store showed "Last Updated"/"Published" as
the Unix epoch in the Toolbox UI regardless of what the server actually
sent (#337).

Parses the ISO-8601 timestamp (the shape the store sends, e.g.
2024-05-12T14:30:00Z, or with a numeric offset) via java.time.Instant,
falling back to 0L for a blank, missing, or malformed string rather than
throwing - matching the field's own empty-string default for a response
that omits it. java.time rather than adding kotlinx-datetime as a new
dependency: this module already uses java.util.* directly in the same
commonMain source set (it has one real target, jvm("desktop")), so nothing
is gained by introducing a second time library for one function.

Tests exercise the real PluginDetailResponse.toPluginInfo(), matching the
convention already established next to it in
PluginStoreResponseDecodingTest: a Z-suffixed timestamp, a numeric-offset
timestamp, a blank string, and a malformed string.

* Fix timestamp normalization edge cases and verify both response mappings

* Use release publication dates and pin timezone-independent fallback

---------

Co-authored-by: Sanjay <sanjaysaini4423@gmail.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(remote-ui): authenticate and expose remote UI surfaces (#348)

* feat(remote-ui): authenticate remote UI process identity

* feat(remote-ui): place authenticated remote UI surfaces

* fix(remote-ui): close ownership and placement lifecycle races

* test(remote-ui): expose placement recovery cases to JUnit

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* feat(dashboard): add shared read-only What's New release feed (#381)

* Add What's New release feed to dashboard

* Serialize update settings persistence

* Remove unused update settings import

* fix(updater): retain realtime refreshes during shared release fetches

* style(updater): format shared release fetch regression tests

* fix(updater): coalesce release refresh bursts and preserve visible history

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(language): consolidate host and out-of-process language-id tables (#358)

* fix(language): consolidate host and out-of-process language-id tables

EditorLanguages (composeApp) and EditorServiceImpl (modules/boss-app-editor)
each hand-maintained their own extension-to-language-id table because
boss-app-editor - a plain JVM module compiled to a GraalVM native image -
could not depend on anything in composeApp. They disagreed: EditorServiceImpl
named .sh/.bash/.zsh "shell", EditorLanguages named them "bash", and
EditorServiceImpl was missing more than forty ids the other table had
(fortran, delphi, latex, lisp, tcl, clojure, batch, diff, ...) (#75).

Moves the table into a new plugin-platform/plugin-language-types module -
dependency-free by design, so it carries nothing extra into boss-app-editor's
native-image build - and has both EditorLanguages and EditorServiceImpl read
from it. This is the first time anything under modules/ depends on
plugin-platform/; verified compiling and passing tests before committing to
the approach.

EditorLanguages keeps its existing public API unchanged (same values for
every extension), so FileTypeCategoriesTest and EditorLanguageDetectionTest
needed no changes. EditorServiceImpl keeps its own "proto"->"protobuf" entry
and "plaintext" fallback as local additions on top of the shared table,
rather than folding them into it: proto isn't part of the boss-file-types.json
default-app-association surface the shared table backs, and "plaintext" is
this service's own gRPC default, not a value composeApp reads.

Adds LanguageIdsTest (the new module) and EditorServiceImplTest (previously
no tests existed for this class at all) - the latter pins the actual bug fix,
that detectLanguage("sh") now returns "bash" instead of "shell".

Out of scope, and disclosed rather than silently dropped: two of the five
duplicate tables the issue names - the editor-tab plugin's own
LanguageDetection and BossEditor's lexer registry - live in separate
repositories this build cannot reach. FileIcons.forSpecialFileName/forFile in
plugin-icons is also not consolidated: it keys icon selection off the raw
filename to preserve distinctions a language id can't carry (package.json,
yarn.lock and pnpm-lock.yaml are all valid JSON/YAML but get three different
brand icons) and never computes a language id in the first place, so
routing it through this table would need a separate icon-selection redesign.

* fix(language): keep file-association drift checks on the shared table

* fix(language): apply shared filename rules to editor file opens

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Stop anon reaching schema public by inheritance, and scope identity to real orgs

The BOSS Supabase anon key is compiled into this repo, which is public. Anything
`anon` can execute is therefore executable by anyone. Verified on 2026-09-08:

  * get_encryption_key() returned the Vault master encryption key, unauthenticated.
    decrypt_text() was anon-callable beside it, making a decryption oracle over
    everything encrypt_text protects - user secrets and the QBO/Google broker
    credentials.
  * find_user_by_email() confirmed any address and returned its uuid.
  * list_shareable_recipients() returned 152 users WITH full email addresses to
    any self-registered account, and 82 @risalabs.ai addresses on a search.
  * arcade_leaderboard() and arcade_bs_standings() published the roster of
    everyone who had opened the Arcade, unauthenticated.

None of it was granted on purpose. PostgreSQL hardwires EXECUTE to PUBLIC on
every new function, PUBLIC includes anon, and this project's default privileges
add anon on top - so a SECURITY DEFINER function is internet-callable from the
moment it is created. Both revokes are traps in mirror image: `from public`
leaves the explicit anon grant, `from anon` leaves the hardwired PUBLIC one, and
ALTER DEFAULT PRIVILEGES cannot remove the latter at all (pg_default_acl loses
the entry and new functions still get `=X`).

  20260908000000  event trigger: revoke PUBLIC+anon on every function created in
                  schema public. An explicit `grant ... to anon` after the create
                  still wins - anon access must be stated, never inherited.
  20260908010000  org_visible_users / org_is_vetted / user_display_name: ONE
                  definition of "may this account learn who that account is",
                  shared by the Arcade and poker rather than copied. Vetted means
                  a human approved the join, which excludes the catch-all `boss`
                  org every account joins on signup (153 members, 20 domains).
  20260908020000  route list_shareable_recipients through both. It was already
                  org-scoped, which is why it looked right; it accepted ANY
                  shared org.
  20260908030000  one-time sweep of the ~30 functions that already had the
                  inherited grant. The RBAC mutators fail closed
                  (is_user_admin(NULL) is false), so this is defence in depth,
                  not a patched escalation.

The rule is a SET, not a per-target predicate: as a per-row qual it is pushed
below a DISTINCT ON and evaluated once per underlying row - 2.7s over 29k score
rows for 45 players, against 37ms.

supabase/audit/identity_disclosure_audit.sql is the standing check, because "we
fixed the leak" is not a durable claim. All four checks report HEALTHY.

Left deliberately anon-callable, each documented where it is kept: the plugin
store browse API, the three helpers called from RLS policies on anon-readable
tables, and custom_access_token_hook. Verified after the sweep that anonymous
plugin-store browse and signed-in RBAC reads both still work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(editor): contain write-local stack overflow and preserve fatal errors (#396)

* Make writeFileContentSafe actually safe: catch the Errors it was named for

`writeFileContentSafe` exists to return false rather than throw. It did not hold
that contract:

    } catch (e: Exception) {
        fileIoLogger.warn(LogCategory.EDITOR, "Error writing file", error = e)
        false
    }

StackOverflowError is an Error, not an Exception, so it went straight through -
out of writeFileContent, out of the editor_write_file MCP handler, and to the
caller as a bare StackOverflowError instead of the handler's own
"Write failed for <path>". The warn that would have named the file never ran
either.

That is not hypothetical. risa-labs-inc/boss-plugin-editor-tab#18 and #27 are two
independent reports of it, on unrelated content, and #27's complaint is precisely
the missing diagnosis: the error "gives no indication of which input caused it".

StackOverflowError and OutOfMemoryError are caught explicitly rather than
widening to Throwable. A stack overflow unwinds and leaves the JVM usable, and
readFileContentSafe in this same file already treats OutOfMemoryError as a
reportable outcome. Throwable would also absorb LinkageError and ThreadDeath,
which are not this function's to swallow - there is a test asserting a
NoClassDefFoundError still propagates.

The failure log now names the path and the character count. Not the content:
these writes carry whatever the user is editing.

**This does not fix the write.** Whatever recurses is upstream of this function,
which is mkdirs plus writeText - I ruled out the plugin's handler, this write,
and the host's MCP argument parsing, and said so on the issue. What changes is
that the failure is reported honestly instead of escaping, which is where a
diagnosis can start.

The body moves to `guardedWrite` with an injectable write, because a test JVM
cannot be made to overflow the stack inside writeText on demand.

Verified: 3729 tests, 0 failures, plus ktlintCheck and detekt.

Refs risa-labs-inc/boss-plugin-editor-tab#18, #27.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(editor): clarify write guard scope and prior recursion fix

* fix(editor): preserve fatal heap errors in write guard

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(state): apply version-checked JSON merge patches (#361)

* Fix: Implement proper ISO timestamp parsing in PluginStoreClient

* Feature: Implement JSON Merge Patch for plugin delta state synchronization

* fix(state): validate delta bases and exclude duplicate timestamp scope

* fix(state): reject malformed and deeply nested patches

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump grpc from 1.83.1 to 1.84.0 (#363)

Bumps `grpc` from 1.83.1 to 1.84.0.

Updates `io.grpc:grpc-netty` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-protobuf` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-stub` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-services` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

Updates `io.grpc:grpc-util` from 1.83.1 to 1.84.0
- [Release notes](https://github.com/grpc/grpc-java/releases)
- [Commits](https://github.com/grpc/grpc-java/compare/v1.83.1...v1.84.0)

---
updated-dependencies:
- dependency-name: io.grpc:grpc-netty
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-protobuf
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-stub
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-services
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: io.grpc:grpc-util
  dependency-version: 1.84.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* :arrow_up: deps:(deps): Bump protobuf from 4.36.0 to 4.36.1 (#364)

Bumps `protobuf` from 4.36.0 to 4.36.1.

Updates `com.google.protobuf:protobuf-kotlin` from 4.36.0 to 4.36.1

Updates `com.google.protobuf:protobuf-java` from 4.36.0 to 4.36.1
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: com.google.protobuf:protobuf-kotlin
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: com.google.protobuf:protobuf-java
  dependency-version: 4.36.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Add the master-key rotation procedure, and rotate

get_encryption_key() was anon-callable, so the Vault master_encryption_key was
retrievable by anyone holding the project anon key - which ships compiled into
this public repo. Revoking access does not un-disclose a key, so it was rotated
on 2026-09-09 and the 184 rows encrypted under it re-encrypted.

Scope of the original exposure, stated precisely because the capability was
worse than the reach: decrypt_text() was anon-callable beside it, so an attacker
holding ciphertext from ANY channel - a backup, an export, a screenshot - could
decrypt it through the API without even needing the key. But no bulk read path
to that ciphertext existed: qbo_token_state and google_token_state have no
client grants at all, and all 178 secrets rows have org_id NULL, so the secrets
policy reduces to owner-only. This was a serious latent vulnerability - one
org-shared secret, one RLS slip, or anyone with backup access would have had
plaintext - rather than a confirmed bulk disclosure.

The script is kept because rotation is not a one-off. It is atomic (one DO
block), self-verifying (md5 of every row's plaintext captured before, re-derived
after through decrypt_text so the check covers both the swap and the data), and
a mismatch or row-count drift raises and rolls back, so a broken rotation cannot
commit. Verified after the run: the live key differs from the disclosed one,
184/184 rows decrypt under it, and the disclosed key opens 0 rows.

The old key is retained in the Vault under an explicit name. Do NOT delete it
while pre-rotation backups exist - they are encrypted under it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(plugins): defer stale jar cleanup until restart (#340)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Require cleanup intent and retain artifacts with unordered manifest versions

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): defer fluck-browser hot-reload to a restart (#352)

* fix(plugins): defer stale jar cleanup until restart

* fix(plugins): defer fluck-browser hot-reload to a restart

Hot-reloading fluck-browser force-unloads the classloader that created its
JxBrowser native views. Every open tab - and every tab opened afterward,
since the factory that would recreate the view is gone too - draws an empty
box, and a second hot-reload does not recover it; only a full restart does.

HotReloadPolicy names plugins that must never be swapped in place, matching
the existing NOT_USER_INSTALLABLE/RetiredPlugins pattern. It's checked at
every path that can force-unload a live plugin to apply an update: the
resetPluginInstances/doReloadPlugin path that actually reproduced this bug,
the menu-driven Reload action, the Toolbox update flow, and the store
installer. For a not-hot-reloadable plugin, the new jar is staged into
installed.json without touching the running instance or its open tabs, and
a status message tells the user to restart to apply it - the next cold
start loads the update with a fresh classloader.

Also fixes two call sites that asked "restart dependent plugins?" before
checking whether an unload was even going to happen, which produced a
confusing prompt for an unload that a deferred update was never going to
do.

* fix(plugins): satisfy quality checks

* Preserve staged plugin artifacts when another plugin updates

* Validate deferred browser artifacts and reject ineffective downgrades

* Require cleanup intent and retain artifacts with unordered manifest versions

* Preserve unloaded-browser recovery and unify safe deferred selection

* Format scoped update cleanup integration

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135) (#365)

* Harden OOP plugin reaping: hook-race flag, registry cleanup, spawn guard (#135)

reapDepth (AtomicInteger) replaces the boolean reaping flag, so the two JVM shutdown hooks (main.kt and KernelBootstrap) cannot clear the in-progress signal while the other is still reaping (item 2). reapChildren now unregisters reaped handles from the process-wide ProcessRegistry, so stale dead entries do not persist across an in-process mode switch (item 4). OutOfProcessPluginSpawnerImpl.spawn() stands down when isReaping(), so a plugin load racing host exit cannot register a child after the reap snapshot (item 3). Covered by ReapChildrenTest.

* Serialize plugin spawn registration with reaping and preserve replacement handles

* Fence late process creation without blocking shutdown and cover recovery

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix(plugins): complete loader cleanup after disposal linkage errors (#302)

* fix: API hot swap during startup

* fix: workflows

* resolved comments by maintainer

* fix CI and build

* Scope disposal error resilience and cover loader cleanup after linkage failure

---------

Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix shortcut rebinding key storage (#329) (#335)

* #329 - Fix: store key names when rebinding shortcuts

* Fold the key spellings Compose renders but the AWT interceptor does not

BOSS maintains two key-name vocabularies over one keyboard.
`AWTKeyboardInterceptor.getKeyName` names a physical key one way; Compose's
`Key.toString()`, which is where `KeymapMatcher` and the Shortcuts screen both
get a name, names it another. Neither list is derived from the other, and where
they disagree a chord fires on one path and silently does nothing on the other.

They had drifted on fourteen keys. Nine are reachable in a running app, because
`Key.toString()` falls through to AWT's `getKeyText`, which answers with the
macOS glyph once the toolkit is up: Enter, Escape, Tab, Backspace, Delete, Home,
End, PageUp and PageDown. So `KeymapMatcher` was asking whether the glyph was
"Tab" and being told no, which takes out Ctrl+Tab and Ctrl+Shift+Tab
(TAB_NEXT and TAB_PREVIOUS) in all four shipped presets. They survive only on the
AWT interceptor, which says "Tab" on both sides. The other five are the cold-JVM
spellings the same call renders before the toolkit is up (`Back Slash`, `Quote`,
`Back Quote`, `Page Up`, `Page Down`), not reachable in a running app but folded
for the same reason.

This is the shape `Left`/`DirectionLeft` and the bracket pair already were, so
the fix is entries in the table those live in rather than a new mechanism.

Also here, because the divergence is what they are about:

- `composeKeyName` is the one place that reads a name out of `Key.toString()`.
  `KeymapMatcher.keyMatches` had its own copy of that parse, and two copies is
  how one path came to know a spelling the other did not.
- `KeyVocabularyAgreementTest` walks the two tables key by key over all 73
  bindable keys. Only the pairing of a Compose `Key` to its AWT keycode is
  hand-written, since that is the identity of the key itself; both vocabularies
  are read from the code under test, so the next divergence fails a build rather
  than a keystroke. A second test asserts the fold does not collapse keys that
  are distinct, which asserting agreement alone would pass without.
- `CanonicalKeyNameTest` enumerates the glyph and spaced spellings, because the
  agreement test's input is whatever the environment renders and a cold run
  cannot reach the glyphs.

`getKeyName` becomes `internal` so the test can read it; the two detekt baseline
entries are rekeyed for the visibility change, with the function unchanged.

Verified warm and cold: 3705 tests in :composeApp:desktopTest, plus ktlintCheck
and detekt. Mutation-checked - reverting the new aliases fails the agreement test
and names every key it lost.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Store a folded key name on capture, and repair the keymaps that hold a keyCode

Completes #329, on top of the one-line capture fix in #335.

which is the right shape and stops new rebinds being dead. Its own description
records the rest as future work: the keymaps already on disk still hold numeric
keys, and nothing tests it. This is that rest.

**Fold the name before storing it.** The raw rendering is not the presets'
vocabulary and it is not stable. The left arrow renders "Left" against the
presets' "DirectionLeft", the right bracket "Close Bracket" against
"CloseBracket", the 1 key "1" against "One" - all of which MATCH through the
alias table, none of which DISPLAY the same, because `formatKeyDisplay` knows
"directionleft" and not "left". Stored raw, a rebound arrow lists as "⌘LEFT"
beside a preset's "⌘←". And `Key.toString()` falls through to AWT's `getKeyText`,
which answers with a word while the toolkit is cold and the macOS glyph once it
is up, so the same user rebinding Tab gets "Tab" or the glyph depending on
nothing they did. `storedKeyName` folds through `canonicalKeyName` first, which
makes a rebind indistinguishable from a preset binding: same match, same
signature, same rendering, and deterministic.

**Existing keymaps are handled twice, deliberately.** `canonicalKeyName` resolves
a stored keyCode so an unmigrated file keeps matching, AND the settings migration
rewrites it so the file stops holding a ten-digit key. Either alone is not
enough: the migration cleans one file, and a keymap restored from a backup,
copied off another machine, or exported and re-imported reaches the matchers
before it reaches the migration.

**The repair runs before `migrateSettings`' chord arithmetic**, which is what
lets #322's drop-on-conflict guard see a UI rebind at all. While those signed as
a numeric key, `chordHolders` produced a signature no preset chord could equal,
so a keymap whose owner had rebound an action through the UI read as not claiming
that chord and the guard handed a new action straight onto it. #322's own
motivating example is "someone who rebound panel.navigate_right", which is a UI
rebind: the guard was protecting hand-edited files and not the ones made in the
app.

Two smaller things:

- The dialog's preview was rendering the raw keyCode too. It had a private copy
  of the display formatter, and now renders the exact `KeyStroke` that Apply
  persists, so the preview cannot disagree with what is saved.
- `KeyBinding.fromComposeKey` had the same `key.keyCode.toString()` line. It has
  no caller today, which is exactly why it would have outlived the fix.

Answering the question the issue leaves open: yes, the Shortcuts screen's own
tester reports these, so it reproduces without a build change.
`ShortcutTestRunner.validateKeyName` reaches its "Unknown key name" branch and
the row goes red.

Verified: 3705 tests in :composeApp:desktopTest, plus ktlintCheck and detekt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Preserve MoveHome and MoveEnd aliases from PR #405

Extract the additional Home/End property-name aliases and assertions from #405. Maintainer integration retains home/end as canonical spellings for the existing vocabulary.

* Keep unrecognised legacy key codes intact during repair

* Exercise warm toolkit key names and correct rendering comments

* Keep shortcut capture consumers consistent and reject modifier-only captures

* Keep import regression setup readable

* Order warm-toolkit test imports per repository style

* Recognise unknown-key diagnostics independently of their localized prefix

* Wrap modifier key lists for quality gates

* Share the dispatcher modifier and lock-key exclusions with capture

* Use public shortcut equality in the formatted menu regression test

* Preserve malformed native codes instead of storing invalid Unicode

* Cover the new dev chrome-density shortcut in capture round trips

---------

Co-authored-by: Arjun Singla <singlaarjun28@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: make sidecar backfill lifecycle reliable (#347)

* fix: make sidecar backfill lifecycle reliable

* test(plugins): format sidecar backfill tests

* fix(plugins): keep backfill persistence on IO and cover lifecycle transitions

* fix(plugins): retry interrupted auth backfill and dispatch the full drain on IO

* style(plugins): format sidecar coordinator declarations

* style(plugins): keep unsigned predicate within line limit

* style(plugins): wrap unsigned predicate expression

* style(plugins): name the current stamp in unsigned predicate

---------

Co-authored-by: ayush <workside@gamil.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* fix: await SupabaseConfig initialization before syncing plugin manifest (#366)

* fix: await SupabaseConfig initialization before syncing plugin manifest

* style: fix ktlint import order for java.io.File

* fix(plugin): wait for SupabaseConfig before starting the manifest sync

SystemPluginManifestService.startSync() ran its startup fetch and Realtime
subscription before SupabaseConfig finished its own async initialize()
call in the Compose UI layer, so both routines' first attempt hit
SupabaseConfig.client's "not initialized" throw: a startup warning for the
fetch, and an unnecessary first trip through the subscription's backoff
retry loop before it ever connects (#370).

SupabaseConfig.isInitialized already exists as a StateFlow for exactly
this. Both of startSync's launched coroutines now suspend on it once,
before touching the client, replacing a guaranteed-to-fail-once startup
path with a wait for the real precondition. The existing "catch up on
(re)connect" comment in subscribeToChanges - added because this same race
could also just lose - stays accurate; this closes the race rather than
only compensating for it afterward.

Test exercises the actual mechanism: a coroutine awaiting
SupabaseConfig.isInitialized genuinely suspends (verified via
TestCoroutineScheduler.runCurrent(), not a coroutine that was merely
scheduled and never run) until initialize() is called, then resolves.

* Isolate manifest readiness regressions from stored auth sessions

* Test manifest readiness without singleton side effects

---------

Co-authored-by: Antriksh1984 <antrikshsingh850@gmail.com>
Co-authored-by: Shivang <shivang.iitk@gmail.com>

* Fix heavyweight modals dismissing destructively on focus loss (#152) (#353)

* Fix heavyweight modals dismissing destructively on focus loss (#152)

Extending LocalHeavyweightOverlays to the whole BossWindow content routed
several dialogs through HeavyweightModal, which dismisses whenever focus
leaves the application (oppositeWindow == null). Two dialogs dismiss to a
destructive action, so this was a regression:

- MemoryPressureNoti…
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.

2 participants