Compare commits

...

49 Commits

Author SHA1 Message Date
dave 488b798275 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-04-02 10:17:28 +00:00
dave 0df19967ca storkit: accept 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-04-02 10:17:22 +00:00
dave 6e04015676 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-04-02 10:17:22 +00:00
dave acaf9477a1 storkit: done 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-04-02 10:15:55 +00:00
dave 46a89d481a storkit: accept 451_bug_chat_test_tsx_help_test_expects_removed_overlay_behavior 2026-04-02 10:11:49 +00:00
dave c51428414e storkit: done 451_bug_chat_test_tsx_help_test_expects_removed_overlay_behavior 2026-04-02 10:11:49 +00:00
Timmy 50405800c6 Bump version to 0.8.5 2026-04-02 11:08:18 +01:00
dave 4aca056bc9 storkit: accept 450_bug_web_ui_silently_swallows_chat_errors_including_oauth_login_link 2026-03-31 18:53:14 +00:00
dave 5e725340b4 storkit: accept 449_bug_oauth_callback_url_ignores_port_cli_flag 2026-03-31 18:52:13 +00:00
dave 3fa2064e3e storkit: done 450_bug_web_ui_silently_swallows_chat_errors_including_oauth_login_link 2026-03-31 14:59:41 +00:00
dave 16f9722851 storkit: merge 450_bug_web_ui_silently_swallows_chat_errors_including_oauth_login_link 2026-03-31 14:59:38 +00:00
dave 5f0680c6c1 storkit: done 449_bug_oauth_callback_url_ignores_port_cli_flag 2026-03-31 14:55:49 +00:00
dave 57e0197d75 storkit: merge 449_bug_oauth_callback_url_ignores_port_cli_flag 2026-03-31 14:55:46 +00:00
dave dc4bac3a85 fix: update /help test to expect botCommand dispatch, fix PTY fd leak in claude_code.rs (#451, #452)
The /help test expected the help overlay to appear, but /help now goes
through botCommand like other slash commands. Updated the test to match.

Also added reader thread join and child.wait() calls to
claude_code.rs to prevent PTY master fd leaks from web UI chat sessions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:48:47 +00:00
dave f16545ec36 fix: join PTY reader thread before returning to prevent stale fd leak (#453)
The reader thread spawned in run_agent_pty_blocking was never joined,
leaving a cloned PTY master fd open after the agent exited. When the
pipeline restarted the agent on the same worktree, the stale fd from
the previous session interfered with the new PTY allocation, causing
Claude Code's bundled ripgrep to crash with:
  fatal runtime error: assertion failed: output.write(&bytes).is_ok()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:41:00 +00:00
dave d132ed8e64 storkit: accept 448_story_send_oauth_login_link_via_chat_when_credentials_are_missing 2026-03-31 14:22:34 +00:00
dave 2a633d604a storkit: create 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-03-31 14:16:32 +00:00
dave 6a44c0b8ee storkit: accept 447_bug_element_tab_completion_display_name_breaks_bot_command_matching 2026-03-31 14:14:51 +00:00
dave 3f97e34f21 storkit: create 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-03-31 14:13:22 +00:00
dave 49a8a23d75 storkit: accept 446_story_oauth_login_button_in_web_ui 2026-03-31 14:08:30 +00:00
dave 1358a32476 storkit: create 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-03-31 14:04:40 +00:00
Dave 9b79160c95 storkit: create 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-03-31 12:25:40 +00:00
Timmy 0cbe99677f Using init: true in docker 2026-03-31 12:36:22 +01:00
dave 46b1609528 storkit: create 453_bug_agent_pty_crashes_with_fatal_runtime_error_on_restart_after_gate_failure 2026-03-31 11:31:05 +00:00
dave 2b0b08ceda storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:30:44 +00:00
dave 19cc684433 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:30:28 +00:00
dave fecb157291 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:25:59 +00:00
dave ac84e7240e storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:21:51 +00:00
dave d5d82bdb00 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:21:45 +00:00
dave f10edd6718 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:17:47 +00:00
dave 3f6cd55833 storkit: create 452_bug_claude_code_pty_crashes_with_fatal_runtime_error_on_agent_restart 2026-03-31 11:13:05 +00:00
dave a9e8bc4d87 storkit: create 451_bug_chat_test_tsx_help_test_expects_removed_overlay_behavior 2026-03-31 11:12:55 +00:00
dave 063e0fa76e storkit: create 450_bug_web_ui_silently_swallows_chat_errors_including_oauth_login_link 2026-03-31 10:55:02 +00:00
dave 9e7bd33822 storkit: create 449_bug_oauth_callback_url_ignores_port_cli_flag 2026-03-31 10:49:23 +00:00
Timmy 7427865e46 Adding more slash commands 2026-03-31 11:33:41 +01:00
Timmy ff5f9c76fd Bump version to 0.8.4 2026-03-31 11:32:10 +01:00
dave 641bbfbe2e storkit: done 448_story_send_oauth_login_link_via_chat_when_credentials_are_missing 2026-03-31 10:28:06 +00:00
dave 5516ec4595 storkit: merge 448_story_send_oauth_login_link_via_chat_when_credentials_are_missing 2026-03-31 10:28:02 +00:00
Timmy 762467efd4 Allowing stat in claude permissions 2026-03-31 11:22:15 +01:00
Timmy 3f54bda360 Updating sha2 2026-03-31 11:21:50 +01:00
dave 4d1e388a48 storkit: done 447_bug_element_tab_completion_display_name_breaks_bot_command_matching 2026-03-31 10:18:24 +00:00
dave 10be86587a storkit: merge 447_bug_element_tab_completion_display_name_breaks_bot_command_matching 2026-03-31 10:18:21 +00:00
dave 6a10591413 storkit: done 446_story_oauth_login_button_in_web_ui 2026-03-31 10:08:43 +00:00
dave 321c88e05e storkit: merge 446_story_oauth_login_button_in_web_ui 2026-03-31 10:08:40 +00:00
dave 23562dfa61 storkit: create 448_story_send_oauth_login_link_via_chat_when_credentials_are_missing 2026-03-31 10:04:26 +00:00
dave cb6ebf1d69 storkit: create 447_bug_element_tab_completion_display_name_breaks_bot_command_matching 2026-03-31 09:58:58 +00:00
Timmy a006985faf Bump version to 0.8.3 2026-03-30 18:17:09 +01:00
dave 3fce9ec082 feat: add Linux arm64 build to release script
Builds aarch64-unknown-linux-musl via cross alongside the existing
x86_64 Linux and macOS arm64 targets.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 17:15:16 +00:00
dave 03026c70cc storkit: create 446_story_oauth_login_button_in_web_ui 2026-03-30 16:27:30 +00:00
35 changed files with 912 additions and 150 deletions
+6 -3
View File
@@ -1,5 +1,7 @@
{ {
"enabledMcpjsonServers": ["storkit"], "enabledMcpjsonServers": [
"storkit"
],
"permissions": { "permissions": {
"allow": [ "allow": [
"Bash(./server/target/debug/storkit:*)", "Bash(./server/target/debug/storkit:*)",
@@ -67,7 +69,8 @@
"Bash(tail *)", "Bash(tail *)",
"Bash(wc *)", "Bash(wc *)",
"Bash(npx vite:*)", "Bash(npx vite:*)",
"Bash(npm run dev:*)" "Bash(npm run dev:*)",
"Bash(stat *)"
] ]
} }
} }
@@ -0,0 +1,48 @@
---
name: "Zombie process accumulation from unrereaped child processes"
---
# Bug 452: Zombie process accumulation from unrereaped child processes
## Description
Storkit accumulates zombie processes over time from unrereaped child and grandchild processes. Observed 101 zombies in Docker container, 27 on macOS host. Breakdown: 51 esbuild, 36 echo, 5 claude, 5 sh, 2 bash, 1 cargo.
Root cause: storkit does not reap orphaned grandchild processes. The zombies are mostly grandchildren (`esbuild`, `echo`, `sh`, `cargo`) spawned by `npm run build`, `cargo test`, etc. during worktree setup and gate checks. This happens both natively (observed 27 zombies on macOS host) and in Docker containers. When the intermediate parent exits, these grandchildren get reparented to storkit (or PID 1 in Docker) and become zombies because nobody calls `waitpid` for them.
**Already fixed:**
- `docker-compose.yml` now has `init: true` which uses tini as PID 1 in Docker — this handles zombie reaping inside containers
- `llm/providers/claude_code.rs` now has `child.wait()` after `child.kill()` in all code paths, and the reader thread is joined before returning
- `agents/pty.rs` reader thread is now joined before returning
**Remaining:** Storkit running natively (e.g. on macOS) still accumulates zombie grandchildren because there is no tini. The fix is to add a background reaper thread that periodically calls `waitpid(-1, WNOHANG)` in a loop to clean up any orphaned children. This should be spawned early in `main()` on Unix platforms. Example:
```rust
#[cfg(unix)]
std::thread::spawn(|| {
loop {
unsafe { while libc::waitpid(-1, std::ptr::null_mut(), libc::WNOHANG) > 0 {} }
std::thread::sleep(std::time::Duration::from_secs(5));
}
});
```
## How to Reproduce
Run several agent sessions. Check with `ps -eo stat,comm | grep Z | awk '{print $2}' | sort | uniq -c | sort -rn`.
## Actual Result
Zombie processes accumulate continuously. Never reaped.
## Expected Result
No zombie accumulation during normal operation.
## Acceptance Criteria
- [x] `child.wait()` is called after `child.kill()` in all code paths in `claude_code.rs`
- [x] Reader threads are joined in both `pty.rs` and `claude_code.rs`
- [x] `init: true` added to docker-compose.yml for Docker deployments
- [ ] Background reaper thread added for native (non-Docker) deployments
- [ ] Verified with `ps aux | grep '<defunct>'` after running multiple agent sessions natively on macOS
@@ -0,0 +1,20 @@
---
name: "OAuth login button in web UI"
---
# Story 446: OAuth login button in web UI
## User Story
As a user of the storkit web UI, I want a login button that triggers the Anthropic OAuth flow, so that I can authenticate without manually navigating to /oauth/authorize.
## Acceptance Criteria
- [ ] Web UI shows a login/authenticate button when no OAuth token is active
- [ ] Clicking the button navigates to /oauth/authorize which starts the Anthropic OAuth flow
- [ ] After successful OAuth callback, the UI updates to show the authenticated state
- [ ] If already authenticated, the button is hidden or shows the current auth status
## Out of Scope
- TBD
@@ -0,0 +1,27 @@
---
name: "Element tab-completion display name breaks bot command matching"
---
# Bug 447: Element tab-completion display name breaks bot command matching
## Description
When a user tab-completes a bot mention in Element, the Matrix client inserts the display name (e.g. `timmy ⚡️`) rather than the user ID (`@timmy`). If the display name contains emoji or special characters, the `strip_bot_mention` function in chat::util may fail to match it against the bot name, causing commands like `ambient on` to not be recognized.
## How to Reproduce
1. Set bot display_name to include emoji (e.g. `timmy ⚡️`) in bot.toml\n2. In Element, tab-complete the bot name to get `timmy ⚡️`\n3. Send `timmy ⚡️ ambient on`\n4. The bot does not respond — command not matched
## Actual Result
Bot ignores the command. The display name with emoji doesn't match during strip_bot_mention, so the command text is not correctly extracted.
## Expected Result
Bot should recognize commands regardless of whether the mention was tab-completed with the display name (including emoji) or typed manually as @localpart.
## Acceptance Criteria
- [ ] strip_bot_mention handles display names containing emoji and special characters
- [ ] strip_bot_mention handles Element's tab-completion format (display name followed by colon or comma)
- [ ] Commands work whether the user types @timmy, timmy, or tab-completes timmy ⚡️
@@ -0,0 +1,20 @@
---
name: "Send OAuth login link via chat when credentials are missing"
---
# Story 448: Send OAuth login link via chat when credentials are missing
## User Story
As a storkit user on Matrix or WhatsApp, I want the bot to send me a clickable OAuth authorize link when credentials are missing or expired, so that I can authenticate without terminal access or manually constructing the URL.
## Acceptance Criteria
- [ ] When storkit detects missing or expired credentials during a chat interaction, it sends the user a clickable /oauth/authorize link
- [ ] Works on Matrix and WhatsApp transports
- [ ] After successful OAuth callback, the user can immediately resume chatting without restarting storkit
- [ ] If credentials are already valid, no login link is sent
## Out of Scope
- TBD
@@ -0,0 +1,27 @@
---
name: "OAuth callback URL ignores --port CLI flag"
---
# Bug 449: OAuth callback URL ignores --port CLI flag
## Description
OAuthState is initialized with `resolve_port()` (reads STORKIT_PORT env var, defaults to 3001) instead of the actual port the server is listening on. When the server is started with `--port 4000`, the OAuth callback URL is still generated as `http://localhost:3001/callback`, so the Anthropic redirect lands on the wrong server and the state parameter lookup fails with "Unknown or expired state parameter".
## How to Reproduce
Start storkit with `--port 4000` (without setting STORKIT_PORT env var). Click the OAuth login button in the web UI. Authenticate with Anthropic. The callback redirect goes to localhost:3001 instead of localhost:4000.
## Actual Result
Callback hits port 3001 (or wrong port). If a different storkit is running there, it returns "Invalid State". If nothing is running there, the page fails to load.
## Expected Result
Callback URL should use the actual server port (from --port CLI flag), so the redirect returns to the correct server instance.
## Acceptance Criteria
- [ ] build_routes receives the actual listening port and passes it to OAuthState::new
- [ ] OAuth callback URL matches the port the server is actually listening on
- [ ] Works with --port flag, STORKIT_PORT env var, and default port
@@ -0,0 +1,27 @@
---
name: "Web UI silently swallows chat errors including OAuth login link"
---
# Bug 450: Web UI silently swallows chat errors including OAuth login link
## Description
When the WebSocket chat returns an error (e.g. OAuth authentication failed with a login URL), the `onError` handler in `Chat.tsx` only logs to `console.error` and resets loading state. The error message is never displayed to the user. This means the OAuth login link from story #448 works on Matrix/WhatsApp but is invisible in the web UI.
## How to Reproduce
Use the web UI with missing or expired OAuth credentials. Send any chat message. The server detects auth failure, attempts token refresh, fails, and returns an error containing a login URL over WebSocket.
## Actual Result
Nothing visible happens. The error is logged to browser console only. The user sees no feedback.
## Expected Result
The error message (including the clickable OAuth login link) should be displayed in the chat as an assistant message so the user can act on it.
## Acceptance Criteria
- [ ] WebSocket error messages are displayed in the chat UI as assistant messages
- [ ] OAuth login URL in the error is rendered as a clickable link
- [ ] Consistent with how Matrix and WhatsApp transports display the same error
@@ -0,0 +1,27 @@
---
name: "Chat.test.tsx /help test expects removed overlay behavior"
---
# Bug 451: Chat.test.tsx /help test expects removed overlay behavior
## Description
The test `AC: /help shows help overlay` in `Chat.test.tsx:1645` expects `/help` to show the `help-overlay` testid element. However, the `/help` intercept was removed from Chat.tsx and `help` was added to `knownCommands`, so `/help` now goes through `api.botCommand()` like other commands. The test needs to be updated to expect a `botCommand("help", ...)` call instead of the overlay. This is blocking gates on stories 449 and 450.
## How to Reproduce
Run `cd frontend && npm test` — the test `AC: /help shows help overlay` fails.
## Actual Result
Test fails: `findByTestId("help-overlay")` times out because the overlay is never rendered. `/help` is dispatched to the backend via `botCommand` instead.
## Expected Result
Test should pass by expecting `/help` to call `api.botCommand("help", ...)` and display the response in chat, consistent with the current code behavior.
## Acceptance Criteria
- [ ] The /help test in Chat.test.tsx is updated to expect botCommand dispatch
- [ ] All frontend tests pass
- [ ] HelpOverlay component and showHelp state can be removed from Chat.tsx if no longer used
@@ -0,0 +1,53 @@
---
name: "Agent PTY crashes with fatal runtime error on restart after gate failure"
---
# Bug 453: Agent PTY crashes with fatal runtime error on restart after gate failure
## Description
When an agent completes coding and the acceptance gates fail (e.g. a test failure), the pipeline restarts the agent on the same worktree. The restarted Claude Code PTY process crashes immediately with `fatal runtime error: assertion failed: output.write(&bytes).is_ok(), aborting`. The process exits in the same second it spawns (Session: None), burns through all 3 retries, and blocks the story.
Key observations:
- The crash is **deterministic, not intermittent**: the first PTY spawn in a worktree always works; the second spawn (restart) always crashes
- Running `claude -p "hello"` manually in the same worktree works fine (no crash) — the issue is specific to spawning via portable-pty
- The worktree is clean (all changes committed) — the agent has nothing to do but fix the gate failure
- The crash is inside the Claude Code binary, not storkit code
- Observed on every story that needed a restart: 329, 400, 420, 438, 446, 449, 450
- Stories that passed gates on the first run were never affected — they never triggered a second spawn
Likely cause: the reader thread spawned by `std::thread::spawn` in `pty.rs` (line 248-255) is never joined. After `run_agent_pty_streaming` returns, the pipeline immediately calls `start_agent` for the retry, but the old reader thread may still be running and holding a cloned PTY reader fd. The new PTY allocation could collide with the still-open fd from the previous session.
The root cause is unknown. It is NOT caused by zombie process accumulation (that is a separate issue in #452).
**Timeline:** The crash first appeared on 2026-03-21. Agent logs go back to 2026-02-23 with no instances before that date. Stories that hit it: 329 (Mar 21), 400 (Mar 26), 420 (Mar 28), 438 (Mar 28), 446 (Mar 30), 449 (Mar 31), 450 (Mar 31).
**Suspect commits around 2026-03-21:**
- `4344081b` — storkit: merge 343_refactor_abstract_agent_runtime_to_support_non_claude_code_backends (refactored agent runtime layer)
- `c4e45b28` — The great storkit name conversion
- Story 359 — Docker security hardening (`cap_drop: ALL`, added back only `SETUID`/`SETGID`) — could affect PTY allocation
- Story 329 — Docker/OrbStack evaluation spike (first crash was on this story's mergemaster)
**Ruled out:** Docker capability restrictions (cap_drop: ALL) — tested by temporarily removing all cap_drop/security_opt; crash still occurs.
**Evidence of stale PTY fd:** After all agents stopped, storkit (PID 7) was still holding an open fd to `/dev/pts/ptmx` (fd 46). This is a leaked PTY master fd from a previous agent session. The reader thread spawned by `std::thread::spawn` in `pty.rs` is never joined, so the cloned reader fd stays open in the storkit process after the agent exits.
Remaining areas to investigate: the unjoined reader thread leaking PTY fds, and whether the leaked fd from the first session interferes with the second PTY allocation.
## How to Reproduce
1. Have a story in current stage with committed code in its worktree. 2. Introduce a test failure that causes gates to fail. 3. The pipeline restarts the agent on the same worktree. 4. The Claude Code process crashes immediately on spawn.
## Actual Result
`fatal runtime error: assertion failed: output.write(&bytes).is_ok(), aborting` — process exits instantly (same second as spawn), Session: None. Burns through retries and blocks the story.
## Expected Result
The restarted agent should start successfully, receive the gate failure context, and be able to fix the issue.
## Acceptance Criteria
- [ ] Agent restart after gate failure successfully spawns a Claude Code PTY session
- [ ] No fatal runtime error on PTY restart in a worktree with prior committed work
- [ ] If Claude Code fails to start, the error is handled gracefully without burning retries
Generated
+171 -113
View File
@@ -26,7 +26,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [ dependencies = [
"crypto-common", "crypto-common 0.1.7",
"generic-array", "generic-array",
] ]
@@ -38,7 +38,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cipher", "cipher",
"cpufeatures", "cpufeatures 0.2.17",
] ]
[[package]] [[package]]
@@ -265,16 +265,16 @@ checksum = "a1d084b0137aaa901caf9f1e8b21daa6aa24d41cd806e111335541eff9683bd6"
[[package]] [[package]]
name = "blake3" name = "blake3"
version = "1.8.3" version = "1.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e"
dependencies = [ dependencies = [
"arrayref", "arrayref",
"arrayvec", "arrayvec",
"cc", "cc",
"cfg-if", "cfg-if",
"constant_time_eq", "constant_time_eq",
"cpufeatures", "cpufeatures 0.3.0",
] ]
[[package]] [[package]]
@@ -286,6 +286,15 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "block-buffer"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
dependencies = [
"hybrid-array",
]
[[package]] [[package]]
name = "block-padding" name = "block-padding"
version = "0.3.3" version = "0.3.3"
@@ -391,7 +400,7 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cipher", "cipher",
"cpufeatures", "cpufeatures 0.2.17",
] ]
[[package]] [[package]]
@@ -427,7 +436,7 @@ version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [ dependencies = [
"crypto-common", "crypto-common 0.1.7",
"inout", "inout",
"zeroize", "zeroize",
] ]
@@ -492,6 +501,12 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]] [[package]]
name = "const_panic" name = "const_panic"
version = "0.2.15" version = "0.2.15"
@@ -551,6 +566,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.5.0" version = "1.5.0"
@@ -596,6 +620,15 @@ dependencies = [
"typenum", "typenum",
] ]
[[package]]
name = "crypto-common"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710"
dependencies = [
"hybrid-array",
]
[[package]] [[package]]
name = "ctr" name = "ctr"
version = "0.9.2" version = "0.9.2"
@@ -612,9 +645,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures 0.2.17",
"curve25519-dalek-derive", "curve25519-dalek-derive",
"digest", "digest 0.10.7",
"fiat-crypto", "fiat-crypto",
"rustc_version", "rustc_version",
"serde", "serde",
@@ -740,7 +773,7 @@ version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [ dependencies = [
"const-oid", "const-oid 0.9.6",
"zeroize", "zeroize",
] ]
@@ -813,11 +846,22 @@ version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer 0.10.4",
"crypto-common", "crypto-common 0.1.7",
"subtle", "subtle",
] ]
[[package]]
name = "digest"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
dependencies = [
"block-buffer 0.12.0",
"const-oid 0.10.2",
"crypto-common 0.2.1",
]
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@@ -862,7 +906,7 @@ dependencies = [
"ed25519", "ed25519",
"rand_core 0.6.4", "rand_core 0.6.4",
"serde", "serde",
"sha2", "sha2 0.10.9",
"subtle", "subtle",
"zeroize", "zeroize",
] ]
@@ -1371,7 +1415,7 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [ dependencies = [
"digest", "digest 0.10.7",
] ]
[[package]] [[package]]
@@ -1452,10 +1496,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hyper" name = "hybrid-array"
version = "1.8.1" version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214"
dependencies = [
"typenum",
]
[[package]]
name = "hyper"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@@ -1468,7 +1521,6 @@ dependencies = [
"httpdate", "httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"pin-utils",
"smallvec", "smallvec",
"tokio", "tokio",
"want", "want",
@@ -1542,12 +1594,13 @@ dependencies = [
[[package]] [[package]]
name = "icu_collections" name = "icu_collections"
version = "2.1.1" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"potential_utf", "potential_utf",
"utf8_iter",
"yoke", "yoke",
"zerofrom", "zerofrom",
"zerovec", "zerovec",
@@ -1555,9 +1608,9 @@ dependencies = [
[[package]] [[package]]
name = "icu_locale_core" name = "icu_locale_core"
version = "2.1.1" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"litemap", "litemap",
@@ -1568,9 +1621,9 @@ dependencies = [
[[package]] [[package]]
name = "icu_normalizer" name = "icu_normalizer"
version = "2.1.1" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
dependencies = [ dependencies = [
"icu_collections", "icu_collections",
"icu_normalizer_data", "icu_normalizer_data",
@@ -1582,15 +1635,15 @@ dependencies = [
[[package]] [[package]]
name = "icu_normalizer_data" name = "icu_normalizer_data"
version = "2.1.1" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
[[package]] [[package]]
name = "icu_properties" name = "icu_properties"
version = "2.1.2" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
dependencies = [ dependencies = [
"icu_collections", "icu_collections",
"icu_locale_core", "icu_locale_core",
@@ -1602,15 +1655,15 @@ dependencies = [
[[package]] [[package]]
name = "icu_properties_data" name = "icu_properties_data"
version = "2.1.2" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
[[package]] [[package]]
name = "icu_provider" name = "icu_provider"
version = "2.1.1" version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"icu_locale_core", "icu_locale_core",
@@ -1862,9 +1915,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.92" version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -1950,9 +2003,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.183" version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]] [[package]]
name = "libredox" name = "libredox"
@@ -1985,9 +2038,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]] [[package]]
name = "litemap" name = "litemap"
version = "0.8.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@@ -2158,7 +2211,7 @@ dependencies = [
"serde", "serde",
"serde_html_form", "serde_html_form",
"serde_json", "serde_json",
"sha2", "sha2 0.10.9",
"tempfile", "tempfile",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
@@ -2251,7 +2304,7 @@ dependencies = [
"ruma", "ruma",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2 0.10.9",
"subtle", "subtle",
"thiserror 2.0.18", "thiserror 2.0.18",
"time", "time",
@@ -2286,7 +2339,7 @@ dependencies = [
"serde", "serde",
"serde-wasm-bindgen", "serde-wasm-bindgen",
"serde_json", "serde_json",
"sha2", "sha2 0.10.9",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tracing", "tracing",
@@ -2340,7 +2393,7 @@ dependencies = [
"rmp-serde", "rmp-serde",
"serde", "serde",
"serde_json", "serde_json",
"sha2", "sha2 0.10.9",
"thiserror 2.0.18", "thiserror 2.0.18",
"zeroize", "zeroize",
] ]
@@ -2599,7 +2652,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_path_to_error", "serde_path_to_error",
"sha2", "sha2 0.10.9",
"thiserror 1.0.69", "thiserror 1.0.69",
"url", "url",
] ]
@@ -2657,7 +2710,7 @@ version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [ dependencies = [
"digest", "digest 0.10.7",
"hmac", "hmac",
] ]
@@ -2711,12 +2764,6 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pkcs8" name = "pkcs8"
version = "0.10.2" version = "0.10.2"
@@ -2842,7 +2889,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [ dependencies = [
"cpufeatures", "cpufeatures 0.2.17",
"opaque-debug", "opaque-debug",
"universal-hash", "universal-hash",
] ]
@@ -2870,9 +2917,9 @@ dependencies = [
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.4" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
dependencies = [ dependencies = [
"zerovec", "zerovec",
] ]
@@ -3504,7 +3551,7 @@ dependencies = [
"rand 0.8.5", "rand 0.8.5",
"ruma-common", "ruma-common",
"serde_json", "serde_json",
"sha2", "sha2 0.10.9",
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
@@ -3552,7 +3599,7 @@ version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
dependencies = [ dependencies = [
"sha2", "sha2 0.10.9",
"walkdir", "walkdir",
] ]
@@ -3826,9 +3873,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "1.1.0" version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
@@ -3876,8 +3923,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures 0.2.17",
"digest", "digest 0.10.7",
] ]
[[package]] [[package]]
@@ -3887,8 +3934,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures 0.2.17",
"digest", "digest 0.10.7",
]
[[package]]
name = "sha2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"digest 0.11.2",
] ]
[[package]] [[package]]
@@ -4019,7 +4077,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]] [[package]]
name = "storkit" name = "storkit"
version = "0.8.2" version = "0.8.5"
dependencies = [ dependencies = [
"async-stream", "async-stream",
"async-trait", "async-trait",
@@ -4046,12 +4104,12 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"serde_yaml", "serde_yaml",
"sha2", "sha2 0.11.0",
"strip-ansi-escapes", "strip-ansi-escapes",
"tempfile", "tempfile",
"tokio", "tokio",
"tokio-tungstenite 0.29.0", "tokio-tungstenite 0.29.0",
"toml 1.1.0+spec-1.1.0", "toml 1.1.2+spec-1.1.0",
"uuid", "uuid",
"wait-timeout", "wait-timeout",
"walkdir", "walkdir",
@@ -4272,9 +4330,9 @@ dependencies = [
[[package]] [[package]]
name = "tinystr" name = "tinystr"
version = "0.8.2" version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"zerovec", "zerovec",
@@ -4398,17 +4456,17 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "1.1.0+spec-1.1.0" version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc" checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde_core", "serde_core",
"serde_spanned", "serde_spanned",
"toml_datetime 1.1.0+spec-1.1.0", "toml_datetime 1.1.1+spec-1.1.0",
"toml_parser", "toml_parser",
"toml_writer", "toml_writer",
"winnow 1.0.0", "winnow 1.0.1",
] ]
[[package]] [[package]]
@@ -4422,39 +4480,39 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "1.1.0+spec-1.1.0" version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [ dependencies = [
"serde_core", "serde_core",
] ]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.25.8+spec-1.1.0" version = "0.25.10+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"toml_datetime 1.1.0+spec-1.1.0", "toml_datetime 1.1.1+spec-1.1.0",
"toml_parser", "toml_parser",
"winnow 1.0.0", "winnow 1.0.1",
] ]
[[package]] [[package]]
name = "toml_parser" name = "toml_parser"
version = "1.1.0+spec-1.1.0" version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [ dependencies = [
"winnow 1.0.0", "winnow 1.0.1",
] ]
[[package]] [[package]]
name = "toml_writer" name = "toml_writer"
version = "1.1.0+spec-1.1.0" version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
[[package]] [[package]]
name = "tower" name = "tower"
@@ -4610,9 +4668,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]] [[package]]
name = "typewit" name = "typewit"
version = "1.14.2" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" checksum = "06fee3a8df48c50c55ad646a4e03b00a370da6fe1850ebf467a8d0165dfcafae"
dependencies = [ dependencies = [
"typewit_proc_macros", "typewit_proc_macros",
] ]
@@ -4681,7 +4739,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [ dependencies = [
"crypto-common", "crypto-common 0.1.7",
"subtle", "subtle",
] ]
@@ -4781,7 +4839,7 @@ dependencies = [
"serde", "serde",
"serde_bytes", "serde_bytes",
"serde_json", "serde_json",
"sha2", "sha2 0.10.9",
"subtle", "subtle",
"thiserror 2.0.18", "thiserror 2.0.18",
"x25519-dalek", "x25519-dalek",
@@ -4851,9 +4909,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.115" version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -4864,9 +4922,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.65" version = "0.4.67"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -4874,9 +4932,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.115" version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -4884,9 +4942,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.115" version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -4897,9 +4955,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.115" version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -4984,9 +5042,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.92" version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -5465,9 +5523,9 @@ checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.0" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@@ -5595,9 +5653,9 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
[[package]] [[package]]
name = "yoke" name = "yoke"
version = "0.8.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
dependencies = [ dependencies = [
"stable_deref_trait", "stable_deref_trait",
"yoke-derive", "yoke-derive",
@@ -5606,9 +5664,9 @@ dependencies = [
[[package]] [[package]]
name = "yoke-derive" name = "yoke-derive"
version = "0.8.1" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -5638,18 +5696,18 @@ dependencies = [
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.6" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
dependencies = [ dependencies = [
"zerofrom-derive", "zerofrom-derive",
] ]
[[package]] [[package]]
name = "zerofrom-derive" name = "zerofrom-derive"
version = "0.1.6" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -5679,9 +5737,9 @@ dependencies = [
[[package]] [[package]]
name = "zerotrie" name = "zerotrie"
version = "0.2.3" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
dependencies = [ dependencies = [
"displaydoc", "displaydoc",
"yoke", "yoke",
@@ -5690,9 +5748,9 @@ dependencies = [
[[package]] [[package]]
name = "zerovec" name = "zerovec"
version = "0.11.5" version = "0.11.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
dependencies = [ dependencies = [
"yoke", "yoke",
"zerofrom", "zerofrom",
@@ -5701,9 +5759,9 @@ dependencies = [
[[package]] [[package]]
name = "zerovec-derive" name = "zerovec-derive"
version = "0.11.2" version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
+1 -1
View File
@@ -21,7 +21,7 @@ rust-embed = "8"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
sha2 = "0.10" sha2 = "0.11.0"
serde_yaml = "0.9" serde_yaml = "0.9"
strip-ansi-escapes = "0.2" strip-ansi-escapes = "0.2"
tempfile = "3" tempfile = "3"
+5
View File
@@ -108,6 +108,11 @@ services:
retries: 3 retries: 3
start_period: 10s start_period: 10s
# Use tini as PID 1 to reap zombie child processes.
# Without this, grandchild processes (esbuild, cargo, etc.) spawned by
# npm/cargo during worktree setup and gate checks become zombies.
init: true
# Restart policy restart on crash but not on manual stop # Restart policy restart on crash but not on manual stop
restart: unless-stopped restart: unless-stopped
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "living-spec-standalone", "name": "living-spec-standalone",
"version": "0.8.2", "version": "0.8.5",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "living-spec-standalone", "name": "living-spec-standalone",
"version": "0.8.2", "version": "0.8.5",
"dependencies": { "dependencies": {
"@types/react-syntax-highlighter": "^15.5.13", "@types/react-syntax-highlighter": "^15.5.13",
"react": "^19.1.0", "react": "^19.1.0",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "living-spec-standalone", "name": "living-spec-standalone",
"private": true, "private": true,
"version": "0.8.2", "version": "0.8.5",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
+7
View File
@@ -19,6 +19,7 @@ vi.mock("./api/client", () => {
setModelPreference: vi.fn(), setModelPreference: vi.fn(),
cancelChat: vi.fn(), cancelChat: vi.fn(),
setAnthropicApiKey: vi.fn(), setAnthropicApiKey: vi.fn(),
getOAuthStatus: vi.fn(),
}; };
class ChatWebSocket { class ChatWebSocket {
connect() {} connect() {}
@@ -65,6 +66,12 @@ describe("App", () => {
mockedApi.getAnthropicApiKeyExists.mockResolvedValue(false); mockedApi.getAnthropicApiKeyExists.mockResolvedValue(false);
mockedApi.getAnthropicModels.mockResolvedValue([]); mockedApi.getAnthropicModels.mockResolvedValue([]);
mockedApi.getModelPreference.mockResolvedValue(null); mockedApi.getModelPreference.mockResolvedValue(null);
mockedApi.getOAuthStatus.mockResolvedValue({
authenticated: false,
expired: false,
expires_at: 0,
has_refresh_token: false,
});
}); });
async function renderApp() { async function renderApp() {
+28 -1
View File
@@ -1,4 +1,5 @@
import * as React from "react"; import * as React from "react";
import type { OAuthStatus } from "./api/client";
import { api } from "./api/client"; import { api } from "./api/client";
import { Chat } from "./components/Chat"; import { Chat } from "./components/Chat";
import { SelectionScreen } from "./components/selection/SelectionScreen"; import { SelectionScreen } from "./components/selection/SelectionScreen";
@@ -14,6 +15,27 @@ function App() {
const [isOpening, setIsOpening] = React.useState(false); const [isOpening, setIsOpening] = React.useState(false);
const [knownProjects, setKnownProjects] = React.useState<string[]>([]); const [knownProjects, setKnownProjects] = React.useState<string[]>([]);
const [homeDir, setHomeDir] = React.useState<string | null>(null); const [homeDir, setHomeDir] = React.useState<string | null>(null);
const [oauthStatus, setOauthStatus] = React.useState<OAuthStatus | null>(
null,
);
React.useEffect(() => {
let active = true;
function fetchOAuthStatus() {
api
.getOAuthStatus()
.then((s) => {
if (active) setOauthStatus(s);
})
.catch(() => {});
}
fetchOAuthStatus();
const intervalId = window.setInterval(fetchOAuthStatus, 5000);
return () => {
active = false;
window.clearInterval(intervalId);
};
}, []);
React.useEffect(() => { React.useEffect(() => {
api api
@@ -182,10 +204,15 @@ function App() {
onCloseSuggestions={closeSuggestions} onCloseSuggestions={closeSuggestions}
completionError={completionError} completionError={completionError}
currentPartial={currentPartial} currentPartial={currentPartial}
oauthStatus={oauthStatus}
/> />
) : ( ) : (
<div className="workspace" style={{ height: "100%" }}> <div className="workspace" style={{ height: "100%" }}>
<Chat projectPath={projectPath} onCloseProject={closeProject} /> <Chat
projectPath={projectPath}
onCloseProject={closeProject}
oauthStatus={oauthStatus}
/>
</div> </div>
)} )}
+11
View File
@@ -205,6 +205,13 @@ export interface CommandOutput {
exit_code: number; exit_code: number;
} }
export interface OAuthStatus {
authenticated: boolean;
expired: boolean;
expires_at: number;
has_refresh_token: boolean;
}
declare const __STORKIT_PORT__: string; declare const __STORKIT_PORT__: string;
const DEFAULT_API_BASE = "/api"; const DEFAULT_API_BASE = "/api";
@@ -402,6 +409,10 @@ export const api = {
deleteStory(storyId: string) { deleteStory(storyId: string) {
return callMcpTool("delete_story", { story_id: storyId }); return callMcpTool("delete_story", { story_id: storyId });
}, },
/** Fetch OAuth status from the server. */
getOAuthStatus() {
return requestJson<OAuthStatus>("/oauth/status", {}, "");
},
/** Execute a bot slash command without LLM invocation. Returns markdown response text. */ /** Execute a bot slash command without LLM invocation. Returns markdown response text. */
botCommand(command: string, args: string, baseUrl?: string) { botCommand(command: string, args: string, baseUrl?: string) {
return requestJson<{ response: string }>( return requestJson<{ response: string }>(
+46 -3
View File
@@ -1642,7 +1642,8 @@ describe("Slash command handling (Story 374)", () => {
expect(mockedApi.botCommand).not.toHaveBeenCalled(); expect(mockedApi.botCommand).not.toHaveBeenCalled();
}); });
it("AC: /help shows help overlay", async () => { it("AC: /help calls botCommand and displays response", async () => {
mockedApi.botCommand.mockResolvedValue({ response: "Available commands: status, help, ..." });
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />); render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull()); await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
@@ -1658,9 +1659,14 @@ describe("Slash command handling (Story 374)", () => {
fireEvent.keyDown(input, { key: "Enter", shiftKey: false }); fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
}); });
expect(await screen.findByTestId("help-overlay")).toBeInTheDocument(); await waitFor(() => {
expect(mockedApi.botCommand).toHaveBeenCalledWith(
"help",
"",
undefined,
);
});
expect(lastSendChatArgs).toBeNull(); expect(lastSendChatArgs).toBeNull();
expect(mockedApi.botCommand).not.toHaveBeenCalled();
}); });
it("AC: botCommand API error shows error message in chat", async () => { it("AC: botCommand API error shows error message in chat", async () => {
@@ -1685,3 +1691,40 @@ describe("Slash command handling (Story 374)", () => {
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
}); });
describe("Bug 450: WebSocket error messages displayed in chat", () => {
beforeEach(() => {
capturedWsHandlers = null;
setupMocks();
});
it("AC1: WebSocket error message is shown in chat as an assistant message", async () => {
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
act(() => {
capturedWsHandlers?.onError("Something went wrong on the server.");
});
expect(
await screen.findByText("Something went wrong on the server."),
).toBeInTheDocument();
});
it("AC2: OAuth login URL in WebSocket error is rendered as a clickable link", async () => {
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
act(() => {
capturedWsHandlers?.onError(
"OAuth login required. Please visit: https://example.com/oauth/login",
);
});
const link = await screen.findByRole("link", { name: /https:\/\/example\.com\/oauth\/login/ });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", "https://example.com/oauth/login");
});
});
+24 -7
View File
@@ -6,6 +6,7 @@ import type { AgentConfigInfo } from "../api/agents";
import { agentsApi } from "../api/agents"; import { agentsApi } from "../api/agents";
import type { import type {
AnthropicModelInfo, AnthropicModelInfo,
OAuthStatus,
PipelineState, PipelineState,
WizardStateData, WizardStateData,
} from "../api/client"; } from "../api/client";
@@ -164,9 +165,14 @@ const getContextWindowSize = (
interface ChatProps { interface ChatProps {
projectPath: string; projectPath: string;
onCloseProject: () => void; onCloseProject: () => void;
oauthStatus?: OAuthStatus | null;
} }
export function Chat({ projectPath, onCloseProject }: ChatProps) { export function Chat({
projectPath,
onCloseProject,
oauthStatus = null,
}: ChatProps) {
const { messages, setMessages, clearMessages } = useChatHistory(projectPath); const { messages, setMessages, clearMessages } = useChatHistory(projectPath);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [model, setModel] = useState("claude-code-pty"); const [model, setModel] = useState("claude-code-pty");
@@ -407,6 +413,14 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
console.error("WebSocket error:", message); console.error("WebSocket error:", message);
setLoading(false); setLoading(false);
setActivityStatus(null); setActivityStatus(null);
const markdownMessage = message.replace(
/(https?:\/\/[^\s]+)/g,
"[$1]($1)",
);
setMessages((prev) => [
...prev,
{ role: "assistant", content: markdownMessage },
]);
if (queuedMessagesRef.current.length > 0) { if (queuedMessagesRef.current.length > 0) {
const batch = queuedMessagesRef.current.map((item) => item.text); const batch = queuedMessagesRef.current.map((item) => item.text);
queuedMessagesRef.current = []; queuedMessagesRef.current = [];
@@ -615,12 +629,6 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
const sendMessage = async (messageText: string) => { const sendMessage = async (messageText: string) => {
if (!messageText.trim()) return; if (!messageText.trim()) return;
// /help — show available slash commands overlay
if (/^\/help\s*$/i.test(messageText)) {
setShowHelp(true);
return;
}
// /reset — clear session and message history without LLM // /reset — clear session and message history without LLM
if (/^\/reset\s*$/i.test(messageText)) { if (/^\/reset\s*$/i.test(messageText)) {
setMessages([]); setMessages([]);
@@ -657,6 +665,14 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
"overview", "overview",
"rebuild", "rebuild",
"loc", "loc",
"help",
"ambient",
"htop",
"rmtree",
"timer",
"unblock",
"unreleased",
"setup",
]); ]);
if (knownCommands.has(cmd)) { if (knownCommands.has(cmd)) {
@@ -940,6 +956,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
enableTools={enableTools} enableTools={enableTools}
onToggleTools={setEnableTools} onToggleTools={setEnableTools}
wsConnected={wsConnected} wsConnected={wsConnected}
oauthStatus={oauthStatus}
/> />
{/* Two-column content area */} {/* Two-column content area */}
+60
View File
@@ -1,4 +1,5 @@
import * as React from "react"; import * as React from "react";
import type { OAuthStatus } from "../api/client";
import { api } from "../api/client"; import { api } from "../api/client";
const { useState, useEffect } = React; const { useState, useEffect } = React;
@@ -32,6 +33,7 @@ interface ChatHeaderProps {
enableTools: boolean; enableTools: boolean;
onToggleTools: (enabled: boolean) => void; onToggleTools: (enabled: boolean) => void;
wsConnected: boolean; wsConnected: boolean;
oauthStatus?: OAuthStatus | null;
} }
const getContextEmoji = (percentage: number): string => { const getContextEmoji = (percentage: number): string => {
@@ -55,6 +57,7 @@ export function ChatHeader({
enableTools, enableTools,
onToggleTools, onToggleTools,
wsConnected, wsConnected,
oauthStatus = null,
}: ChatHeaderProps) { }: ChatHeaderProps) {
const hasModelOptions = availableModels.length > 0 || claudeModels.length > 0; const hasModelOptions = availableModels.length > 0 || claudeModels.length > 0;
const [showConfirm, setShowConfirm] = useState(false); const [showConfirm, setShowConfirm] = useState(false);
@@ -340,6 +343,63 @@ export function ChatHeader({
</div> </div>
<div style={{ display: "flex", alignItems: "center", gap: "16px" }}> <div style={{ display: "flex", alignItems: "center", gap: "16px" }}>
{oauthStatus !== null &&
(!oauthStatus.authenticated || oauthStatus.expired) && (
<button
type="button"
title="Authenticate with Claude via OAuth"
onClick={() => {
window.open(
"/oauth/authorize",
"_blank",
"noopener,noreferrer",
);
}}
style={{
padding: "6px 12px",
borderRadius: "99px",
border: "none",
fontSize: "0.85em",
backgroundColor: "#1a3a5c",
color: "#7eb8f7",
cursor: "pointer",
outline: "none",
transition: "all 0.2s",
whiteSpace: "nowrap",
}}
onMouseOver={(e) => {
e.currentTarget.style.backgroundColor = "#234d7a";
e.currentTarget.style.color = "#a8d4ff";
}}
onMouseOut={(e) => {
e.currentTarget.style.backgroundColor = "#1a3a5c";
e.currentTarget.style.color = "#7eb8f7";
}}
onFocus={(e) => {
e.currentTarget.style.backgroundColor = "#234d7a";
e.currentTarget.style.color = "#a8d4ff";
}}
onBlur={(e) => {
e.currentTarget.style.backgroundColor = "#1a3a5c";
e.currentTarget.style.color = "#7eb8f7";
}}
>
{oauthStatus.expired ? "Re-authenticate" : "Login with Claude"}
</button>
)}
{oauthStatus?.authenticated && !oauthStatus.expired && (
<span
title="Authenticated with Claude via OAuth"
style={{
fontSize: "0.8em",
color: "#4caf50",
whiteSpace: "nowrap",
}}
>
Claude
</span>
)}
<div <div
style={{ style={{
fontSize: "0.75em", fontSize: "0.75em",
@@ -1,4 +1,5 @@
import type { KeyboardEvent } from "react"; import type { KeyboardEvent } from "react";
import type { OAuthStatus } from "../../api/client";
import { ProjectPathInput } from "./ProjectPathInput.tsx"; import { ProjectPathInput } from "./ProjectPathInput.tsx";
import { RecentProjectsList } from "./RecentProjectsList.tsx"; import { RecentProjectsList } from "./RecentProjectsList.tsx";
@@ -24,6 +25,7 @@ export interface SelectionScreenProps {
onCloseSuggestions: () => void; onCloseSuggestions: () => void;
completionError: string | null; completionError: string | null;
currentPartial: string; currentPartial: string;
oauthStatus?: OAuthStatus | null;
} }
export function SelectionScreen({ export function SelectionScreen({
@@ -43,6 +45,7 @@ export function SelectionScreen({
onCloseSuggestions, onCloseSuggestions,
completionError, completionError,
currentPartial, currentPartial,
oauthStatus = null,
}: SelectionScreenProps) { }: SelectionScreenProps) {
const resolvedHomeDir = homeDir const resolvedHomeDir = homeDir
? homeDir.endsWith("/") ? homeDir.endsWith("/")
@@ -57,6 +60,43 @@ export function SelectionScreen({
<h1>Storkit</h1> <h1>Storkit</h1>
<p>Paste or complete a project path to start.</p> <p>Paste or complete a project path to start.</p>
{oauthStatus !== null && (
<div style={{ marginBottom: "1rem" }}>
{!oauthStatus.authenticated || oauthStatus.expired ? (
<button
type="button"
onClick={() => {
window.open(
"/oauth/authorize",
"_blank",
"noopener,noreferrer",
);
}}
style={{
padding: "8px 16px",
borderRadius: "6px",
border: "1px solid #1a3a5c",
backgroundColor: "#1a3a5c",
color: "#7eb8f7",
cursor: "pointer",
fontSize: "0.9em",
}}
>
{oauthStatus.expired
? "Re-authenticate with Claude"
: "Login with Claude"}
</button>
) : (
<span
title="Authenticated with Claude via OAuth"
style={{ color: "#4caf50", fontSize: "0.9em" }}
>
Authenticated with Claude
</span>
)}
</div>
)}
{knownProjects.length > 0 && ( {knownProjects.length > 0 && (
<RecentProjectsList <RecentProjectsList
projects={knownProjects} projects={knownProjects}
+38
View File
@@ -50,10 +50,48 @@ export const SLASH_COMMANDS: SlashCommand[] = [
name: "/overview <number>", name: "/overview <number>",
description: "Show the implementation summary for a merged story.", description: "Show the implementation summary for a merged story.",
}, },
{
name: "/ambient",
description: "Toggle ambient mode: `/ambient on` or `/ambient off`.",
},
{
name: "/htop",
description:
"Show live system and agent process dashboard: `/htop`, `/htop 10m`, `/htop stop`.",
},
{
name: "/loc",
description:
"Show top source files by line count: `/loc` (top 10), `/loc <N>`, or `/loc <filepath>`.",
},
{
name: "/rmtree <number>",
description:
"Delete the worktree for a story without removing it from the pipeline.",
},
{ {
name: "/rebuild", name: "/rebuild",
description: "Rebuild the server binary and restart.", description: "Rebuild the server binary and restart.",
}, },
{
name: "/timer",
description:
"Schedule a deferred agent start: `/timer <story_id> <HH:MM>`, `/timer list`, `/timer cancel <story_id>`.",
},
{
name: "/unblock <number>",
description:
"Reset a blocked story: clears blocked flag and resets retry count.",
},
{
name: "/unreleased",
description: "Show stories merged to master since the last release tag.",
},
{
name: "/setup",
description:
"Show setup wizard progress; or `/setup confirm` / `/setup skip` / `/setup retry` to drive the wizard.",
},
{ {
name: "/reset", name: "/reset",
description: description:
+14
View File
@@ -30,6 +30,20 @@ export default defineConfig(() => {
proxy.on("error", (_err) => {}); proxy.on("error", (_err) => {});
}, },
}, },
"/oauth": {
target: `http://127.0.0.1:${String(backendPort)}`,
timeout: 120000,
configure: (proxy) => {
proxy.on("error", (_err) => {});
},
},
"/callback": {
target: `http://127.0.0.1:${String(backendPort)}`,
timeout: 120000,
configure: (proxy) => {
proxy.on("error", (_err) => {});
},
},
}, },
watch: { watch: {
ignored: [ ignored: [
+5 -1
View File
@@ -81,9 +81,12 @@ echo "==> Releasing ${TAG}"
echo "==> Building macOS (native)..." echo "==> Building macOS (native)..."
cargo build --release cargo build --release
echo "==> Building Linux (static musl via cross)..." echo "==> Building Linux amd64 (static musl via cross)..."
cross build --release --target x86_64-unknown-linux-musl cross build --release --target x86_64-unknown-linux-musl
echo "==> Building Linux arm64 (static musl via cross)..."
cross build --release --target aarch64-unknown-linux-musl
# ── Package ──────────────────────────────────────────────────── # ── Package ────────────────────────────────────────────────────
DIST="target/dist" DIST="target/dist"
rm -rf "$DIST" rm -rf "$DIST"
@@ -91,6 +94,7 @@ mkdir -p "$DIST"
cp "target/release/${BINARY_NAME}" "${DIST}/${BINARY_NAME}-macos-arm64" cp "target/release/${BINARY_NAME}" "${DIST}/${BINARY_NAME}-macos-arm64"
cp "target/x86_64-unknown-linux-musl/release/${BINARY_NAME}" "${DIST}/${BINARY_NAME}-linux-amd64" cp "target/x86_64-unknown-linux-musl/release/${BINARY_NAME}" "${DIST}/${BINARY_NAME}-linux-amd64"
cp "target/aarch64-unknown-linux-musl/release/${BINARY_NAME}" "${DIST}/${BINARY_NAME}-linux-arm64"
chmod +x "${DIST}"/* chmod +x "${DIST}"/*
echo "==> Binaries:" echo "==> Binaries:"
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "storkit" name = "storkit"
version = "0.8.2" version = "0.8.5"
edition = "2024" edition = "2024"
build = "build.rs" build = "build.rs"
+13 -1
View File
@@ -245,13 +245,16 @@ fn run_agent_pty_blocking(
// via recv_timeout: if no output arrives within the configured window // via recv_timeout: if no output arrives within the configured window
// the process is killed and the agent is marked Failed. // the process is killed and the agent is marked Failed.
let (line_tx, line_rx) = std::sync::mpsc::channel::<std::io::Result<String>>(); let (line_tx, line_rx) = std::sync::mpsc::channel::<std::io::Result<String>>();
std::thread::spawn(move || { let sid_for_reader = story_id.to_string();
let aname_for_reader = agent_name.to_string();
let reader_handle = std::thread::spawn(move || {
let buf_reader = BufReader::new(reader); let buf_reader = BufReader::new(reader);
for line in buf_reader.lines() { for line in buf_reader.lines() {
if line_tx.send(line).is_err() { if line_tx.send(line).is_err() {
break; break;
} }
} }
slog!("[agent:{sid_for_reader}:{aname_for_reader}] Reader thread exiting");
}); });
let timeout_dur = if inactivity_timeout_secs > 0 { let timeout_dur = if inactivity_timeout_secs > 0 {
@@ -424,6 +427,15 @@ fn run_agent_pty_blocking(
let _ = child.kill(); let _ = child.kill();
let _ = child.wait(); let _ = child.wait();
// Wait for the reader thread to finish so it releases the cloned PTY
// master fd before we return. Without this, the next PTY spawn for the
// same story can collide with a still-open fd from this session (#453).
slog!("[agent:{story_id}:{agent_name}] Waiting for reader thread to exit");
if let Err(e) = reader_handle.join() {
slog!("[agent:{story_id}:{agent_name}] Reader thread panicked: {e:?}");
}
slog!("[agent:{story_id}:{agent_name}] Reader thread joined");
slog!( slog!(
"[agent:{story_id}:{agent_name}] Done. Session: {:?}", "[agent:{story_id}:{agent_name}] Done. Session: {:?}",
session_id session_id
@@ -616,7 +616,13 @@ pub(super) async fn handle_message(
} }
Err(e) => { Err(e) => {
slog!("[matrix-bot] LLM error: {e}"); slog!("[matrix-bot] LLM error: {e}");
let err_msg = format!("Error processing your request: {e}"); let err_msg = if let Some(url) = crate::llm::oauth::extract_login_url_from_error(&e) {
format!(
"Authentication required. [Click here to log in to Claude]({url})"
)
} else {
format!("Error processing your request: {e}")
};
let _ = msg_tx.send(err_msg.clone()); let _ = msg_tx.send(err_msg.clone());
(err_msg, None) (err_msg, None)
} }
@@ -686,6 +692,24 @@ mod tests {
assert_eq!(prompt, "@bob:example.com: What's up?"); assert_eq!(prompt, "@bob:example.com: What's up?");
} }
// -- OAuth login link formatting ----------------------------------------
#[test]
fn oauth_error_produces_login_link() {
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize";
let url = crate::llm::oauth::extract_login_url_from_error(err);
assert!(url.is_some(), "should extract URL from OAuth error");
let msg = format!("Authentication required. [Click here to log in to Claude]({})", url.unwrap());
assert!(msg.contains("http://localhost:3001/oauth/authorize"));
assert!(msg.contains("[Click here to log in to Claude]"));
}
#[test]
fn non_oauth_error_not_formatted_as_link() {
let err = "Some unrelated error";
assert!(crate::llm::oauth::extract_login_url_from_error(err).is_none());
}
// -- bot_name / system prompt ------------------------------------------- // -- bot_name / system prompt -------------------------------------------
#[test] #[test]
+25 -1
View File
@@ -383,7 +383,13 @@ async fn handle_llm_message(ctx: &WhatsAppWebhookContext, sender: &str, user_mes
} }
Err(e) => { Err(e) => {
slog!("[whatsapp] LLM error: {e}"); slog!("[whatsapp] LLM error: {e}");
let err_msg = format!("Error processing your request: {e}"); let err_msg = if let Some(url) = crate::llm::oauth::extract_login_url_from_error(&e) {
format!(
"Authentication required. Log in to Claude here: {url}"
)
} else {
format!("Error processing your request: {e}")
};
let _ = msg_tx.send(err_msg.clone()); let _ = msg_tx.send(err_msg.clone());
(err_msg, None) (err_msg, None)
} }
@@ -491,6 +497,24 @@ mod tests {
}) })
} }
// ── OAuth login link formatting ───────────────────────────────────────
#[test]
fn whatsapp_oauth_error_produces_plain_text_url() {
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize";
let url = crate::llm::oauth::extract_login_url_from_error(err);
assert!(url.is_some(), "should extract URL from OAuth error");
let msg = format!("Authentication required. Log in to Claude here: {}", url.unwrap());
assert!(msg.contains("http://localhost:3001/oauth/authorize"));
assert!(!msg.contains('['), "WhatsApp message should not use Markdown link syntax");
}
#[test]
fn whatsapp_non_oauth_error_not_formatted_as_link() {
let err = "Some unrelated error occurred during processing";
assert!(crate::llm::oauth::extract_login_url_from_error(err).is_none());
}
// ── Allowlist tests ─────────────────────────────────────────────────── // ── Allowlist tests ───────────────────────────────────────────────────
#[tokio::test] #[tokio::test]
+49 -5
View File
@@ -46,29 +46,44 @@ pub fn strip_prefix_ci<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
/// - `@bot_localpart:server.com rest` → `rest` /// - `@bot_localpart:server.com rest` → `rest`
/// - `@bot_localpart rest` → `rest` /// - `@bot_localpart rest` → `rest`
/// - `DisplayName rest` → `rest` /// - `DisplayName rest` → `rest`
/// - `DisplayName: rest` → `rest` (Element tab-completion inserts a colon)
/// - `DisplayName, rest` → `rest` (Element tab-completion may insert a comma)
/// - `DisplayName ⚡️: rest` → `rest` (display name with emoji)
pub fn strip_bot_mention<'a>(message: &'a str, bot_name: &str, bot_user_id: &str) -> &'a str { pub fn strip_bot_mention<'a>(message: &'a str, bot_name: &str, bot_user_id: &str) -> &'a str {
let trimmed = message.trim(); let trimmed = message.trim();
// Try full Matrix user ID (e.g. "@timmy:homeserver.local") // Try full Matrix user ID (e.g. "@timmy:homeserver.local")
if let Some(rest) = strip_prefix_ci(trimmed, bot_user_id) { if let Some(rest) = strip_prefix_ci(trimmed, bot_user_id) {
return rest; return strip_mention_separator(rest);
} }
// Try @localpart (e.g. "@timmy") // Try @localpart (e.g. "@timmy")
if let Some(localpart) = bot_user_id.split(':').next() if let Some(localpart) = bot_user_id.split(':').next()
&& let Some(rest) = strip_prefix_ci(trimmed, localpart) && let Some(rest) = strip_prefix_ci(trimmed, localpart)
{ {
return rest; return strip_mention_separator(rest);
} }
// Try display name (e.g. "Timmy") // Try display name (e.g. "Timmy" or "timmy ⚡️")
if let Some(rest) = strip_prefix_ci(trimmed, bot_name) { if let Some(rest) = strip_prefix_ci(trimmed, bot_name) {
return rest; return strip_mention_separator(rest);
} }
trimmed trimmed
} }
/// Strip an optional Element tab-completion separator (`:` or `,`) and
/// surrounding whitespace from the start of text that follows a bot mention.
///
/// Element's tab-completion inserts `DisplayName: ` (colon + space) after the
/// name. Without this strip the leading `:` would be treated as part of the
/// command name and no command would match.
fn strip_mention_separator(rest: &str) -> &str {
let rest = rest.trim_start();
let rest = rest.strip_prefix([',', ':']).unwrap_or(rest);
rest.trim_start()
}
/// Returns `true` when `text` ends while inside an open fenced code block. /// Returns `true` when `text` ends while inside an open fenced code block.
/// ///
/// A fenced code block opens and closes on lines that start with ` ``` ` /// A fenced code block opens and closes on lines that start with ` ``` `
@@ -334,7 +349,36 @@ mod tests {
#[test] #[test]
fn strip_mention_comma_after_name() { fn strip_mention_comma_after_name() {
let rest = strip_bot_mention("@timmy, help", "Timmy", "@timmy:homeserver.local"); let rest = strip_bot_mention("@timmy, help", "Timmy", "@timmy:homeserver.local");
assert_eq!(rest.trim().trim_start_matches(',').trim(), "help"); assert_eq!(rest.trim(), "help");
}
#[test]
fn strip_mention_colon_separator_element_tab_completion() {
// Element tab-completes display names with a trailing ": "
let rest = strip_bot_mention(
"timmy ⚡️: ambient on",
"timmy ⚡️",
"@timmy:homeserver.local",
);
assert_eq!(rest, "ambient on");
}
#[test]
fn strip_mention_emoji_display_name_no_separator() {
// Display name with emoji, no separator
let rest = strip_bot_mention(
"timmy ⚡️ ambient on",
"timmy ⚡️",
"@timmy:homeserver.local",
);
assert_eq!(rest, "ambient on");
}
#[test]
fn strip_mention_colon_after_localpart() {
// Element may also produce "@timmy: help"
let rest = strip_bot_mention("@timmy: help", "Timmy", "@timmy:homeserver.local");
assert_eq!(rest, "help");
} }
// -- drain_complete_paragraphs ------------------------------------------ // -- drain_complete_paragraphs ------------------------------------------
+12 -2
View File
@@ -64,12 +64,13 @@ pub fn build_routes(
ctx: AppContext, ctx: AppContext,
whatsapp_ctx: Option<Arc<WhatsAppWebhookContext>>, whatsapp_ctx: Option<Arc<WhatsAppWebhookContext>>,
slack_ctx: Option<Arc<SlackWebhookContext>>, slack_ctx: Option<Arc<SlackWebhookContext>>,
port: u16,
) -> impl poem::Endpoint { ) -> impl poem::Endpoint {
let ctx_arc = std::sync::Arc::new(ctx); let ctx_arc = std::sync::Arc::new(ctx);
let (api_service, docs_service) = build_openapi_service(ctx_arc.clone()); let (api_service, docs_service) = build_openapi_service(ctx_arc.clone());
let oauth_state = Arc::new(oauth::OAuthState::new(resolve_port())); let oauth_state = Arc::new(oauth::OAuthState::new(port));
let mut route = Route::new() let mut route = Route::new()
.nest("/api", api_service) .nest("/api", api_service)
@@ -236,6 +237,15 @@ mod tests {
fn build_routes_constructs_without_panic() { fn build_routes_constructs_without_panic() {
let tmp = tempfile::tempdir().unwrap(); let tmp = tempfile::tempdir().unwrap();
let ctx = context::AppContext::new_test(tmp.path().to_path_buf()); let ctx = context::AppContext::new_test(tmp.path().to_path_buf());
let _endpoint = build_routes(ctx, None, None); let _endpoint = build_routes(ctx, None, None, 3001);
}
#[test]
fn build_routes_accepts_custom_port() {
// Verify build_routes compiles and runs with a non-default port,
// ensuring the port parameter flows through to OAuthState.
let tmp = tempfile::tempdir().unwrap();
let ctx = context::AppContext::new_test(tmp.path().to_path_buf());
let _endpoint = build_routes(ctx, None, None, 9999);
} }
} }
+7
View File
@@ -423,6 +423,13 @@ mod tests {
assert_eq!(state.callback_url(), "http://localhost:3001/callback"); assert_eq!(state.callback_url(), "http://localhost:3001/callback");
} }
#[test]
fn oauth_state_callback_url_uses_given_port() {
// Ensure OAuthState::new uses the port passed to it, not a hardcoded value.
let state = OAuthState::new(9876);
assert_eq!(state.callback_url(), "http://localhost:9876/callback");
}
#[tokio::test] #[tokio::test]
async fn html_response_contains_title_and_message() { async fn html_response_contains_title_and_message() {
let resp = html_response(StatusCode::OK, "Test Title", "Test message"); let resp = html_response(StatusCode::OK, "Test Title", "Test message");
+33
View File
@@ -161,10 +161,43 @@ pub async fn refresh_access_token() -> Result<(), String> {
Ok(()) Ok(())
} }
/// Extract the OAuth login URL from an error message produced by the Claude Code provider.
///
/// The provider returns errors like:
/// `"OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize"`
///
/// Returns the URL portion when the error indicates missing or expired credentials,
/// `None` otherwise.
pub fn extract_login_url_from_error(err: &str) -> Option<&str> {
let marker = "Please log in: ";
let start = err.find(marker)?;
Some(err[start + marker.len()..].trim())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn extract_login_url_from_oauth_error() {
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3001/oauth/authorize";
let url = extract_login_url_from_error(err);
assert_eq!(url, Some("http://localhost:3001/oauth/authorize"));
}
#[test]
fn extract_login_url_returns_none_for_unrelated_error() {
let err = "Some other error occurred";
assert!(extract_login_url_from_error(err).is_none());
}
#[test]
fn extract_login_url_with_different_port() {
let err = "OAuth session expired or credentials missing. Please log in: http://localhost:3002/oauth/authorize";
let url = extract_login_url_from_error(err);
assert_eq!(url, Some("http://localhost:3002/oauth/authorize"));
}
#[test] #[test]
fn parse_credentials_file() { fn parse_credentials_file() {
let json = r#"{ let json = r#"{
+10 -3
View File
@@ -138,9 +138,11 @@ impl ClaudeCodeProvider {
on_token("\n*Refreshing authentication token...*\n"); on_token("\n*Refreshing authentication token...*\n");
continue; continue;
} }
Err(e) => { Err(_e) => {
let port = crate::http::resolve_port();
let login_url = format!("http://localhost:{port}/oauth/authorize");
return Err(format!( return Err(format!(
"OAuth session expired. Please run `claude login` to re-authenticate. ({e})" "OAuth session expired or credentials missing. Please log in: {login_url}"
)); ));
} }
} }
@@ -254,7 +256,7 @@ fn run_pty_session(
// Read NDJSON lines from stdout // Read NDJSON lines from stdout
let (line_tx, line_rx) = std::sync::mpsc::channel::<Option<String>>(); let (line_tx, line_rx) = std::sync::mpsc::channel::<Option<String>>();
std::thread::spawn(move || { let reader_handle = std::thread::spawn(move || {
let buf_reader = BufReader::new(reader); let buf_reader = BufReader::new(reader);
slog!("[pty-debug] Reader thread started"); slog!("[pty-debug] Reader thread started");
for line in buf_reader.lines() { for line in buf_reader.lines() {
@@ -282,6 +284,8 @@ fn run_pty_session(
loop { loop {
if cancelled.load(Ordering::Relaxed) { if cancelled.load(Ordering::Relaxed) {
let _ = child.kill(); let _ = child.kill();
let _ = child.wait();
let _ = reader_handle.join();
return Err("Cancelled".to_string()); return Err("Cancelled".to_string());
} }
@@ -363,8 +367,11 @@ fn run_pty_session(
} }
// If still running after 2s, kill it // If still running after 2s, kill it
let _ = child.kill(); let _ = child.kill();
let _ = child.wait();
} }
} }
// Wait for the reader thread to release the cloned PTY master fd.
let _ = reader_handle.join();
Ok(()) Ok(())
} }
+1 -1
View File
@@ -495,7 +495,7 @@ async fn main() -> Result<(), std::io::Error> {
matrix_shutdown_tx: Some(Arc::clone(&matrix_shutdown_tx)), matrix_shutdown_tx: Some(Arc::clone(&matrix_shutdown_tx)),
}; };
let app = build_routes(ctx, whatsapp_ctx.clone(), slack_ctx.clone()); let app = build_routes(ctx, whatsapp_ctx.clone(), slack_ctx.clone(), port);
// Optional Matrix bot: connect to the homeserver and start listening for // Optional Matrix bot: connect to the homeserver and start listening for
// messages if `.storkit/bot.toml` is present and enabled. // messages if `.storkit/bot.toml` is present and enabled.
+28 -3
View File
@@ -123,7 +123,32 @@ pub async fn rebuild_and_restart(
workspace_root.display() workspace_root.display()
); );
// 3. Build the server binary, matching the current build profile so the // 3. Rebuild the frontend bundle so rust-embed picks up the latest assets.
let frontend_dir = workspace_root.join("frontend");
if frontend_dir.join("package.json").exists() {
slog!("[rebuild] Building frontend");
let fe_output = tokio::task::spawn_blocking({
let frontend_dir = frontend_dir.clone();
move || {
std::process::Command::new("npm")
.args(["run", "build"])
.current_dir(&frontend_dir)
.output()
}
})
.await
.map_err(|e| format!("Frontend build task panicked: {e}"))?
.map_err(|e| format!("Failed to run npm run build: {e}"))?;
if !fe_output.status.success() {
let stderr = String::from_utf8_lossy(&fe_output.stderr);
slog!("[rebuild] Frontend build failed:\n{stderr}");
return Err(format!("Frontend build failed:\n{stderr}"));
}
slog!("[rebuild] Frontend build succeeded");
}
// 4. Build the server binary, matching the current build profile so the
// re-exec via current_exe() picks up the new binary. // re-exec via current_exe() picks up the new binary.
let build_args: Vec<&str> = if cfg!(debug_assertions) { let build_args: Vec<&str> = if cfg!(debug_assertions) {
vec!["build", "-p", "storkit"] vec!["build", "-p", "storkit"]
@@ -152,14 +177,14 @@ pub async fn rebuild_and_restart(
slog!("[rebuild] Build succeeded, re-execing with new binary"); slog!("[rebuild] Build succeeded, re-execing with new binary");
// 4. Send shutdown notification before replacing the process so that chat // 5. Send shutdown notification before replacing the process so that chat
// participants know the bot is going offline. Best-effort only — we // participants know the bot is going offline. Best-effort only — we
// do not abort the rebuild if the send fails. // do not abort the rebuild if the send fails.
if let Some(n) = notifier { if let Some(n) = notifier {
n.notify(ShutdownReason::Rebuild).await; n.notify(ShutdownReason::Rebuild).await;
} }
// 5. Re-exec with the new binary. // 6. Re-exec with the new binary.
// Use the cargo output path rather than current_exe() so that rebuilds // Use the cargo output path rather than current_exe() so that rebuilds
// inside Docker work correctly — the running binary may be installed at // inside Docker work correctly — the running binary may be installed at
// /usr/local/bin/storkit (read-only) while cargo writes the new binary // /usr/local/bin/storkit (read-only) while cargo writes the new binary