fix(ci): resolve failing typecheck and audit jobs - #5
Conversation
The audit job runs `uv audit`, which failed on 65 advisories across 15 locked packages (urllib3, starlette, pyjwt, python-multipart, authlib, click, cryptography, idna, mcp, pydantic-settings, pymdown-extensions, pytest, setuptools, torch, transformers). Upgrade only those packages rather than running a blanket `uv lock --upgrade`: a full upgrade also bumps ruff, which then reports 124 new lint errors and would trade a red audit job for a red lint job. `uv audit` now reports no known vulnerabilities in 194 packages.
The typecheck job synced with `uv sync --all-packages`, which installs each workspace package's default dependencies but not its extras. studyctl keeps fastapi in the `web` extra, so pyright could not resolve `fastapi` in src/studyctl/web/ and failed with 21 reportMissingImports errors. Add --all-extras to the sync step that precedes pyright. publish.yml and pre-release.yml ran pyright after the same incomplete sync, so they carried the identical latent defect and would have failed on the next release. Also bump actions/checkout v4.2.2 -> v7.0.1 and astral-sh/setup-uv v4 -> v9.0.0 across all workflows. Both targeted Node 20, which is deprecated and currently force-run on Node 24; this removes the warning before it becomes an error. Existing pin style is preserved per file (SHA pins in ci.yml and nightly-uat.yml, floating tags elsewhere).
There was a problem hiding this comment.
Pull request overview
This PR addresses failing CI on main by adjusting uv dependency installation to satisfy pyright’s import resolution, updating the lockfile to eliminate uv audit advisories, and bumping GitHub Actions used in workflows to newer Node-compatible versions.
Changes:
- Update CI/release workflows to install uv extras (and bump
actions/checkout/setup-uv) so pyright can resolvestudyctl’s optional web dependencies. - Refresh
uv.lockwith targeted dependency upgrades to clearuv auditadvisories. - Apply the same workflow fixes across release-related workflows (
publish.yml,pre-release.yml) and other scheduled/docs workflows for consistency.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
uv.lock |
Updates locked dependencies to resolve audit advisories and refresh resolution metadata. |
.github/workflows/ci.yml |
Adjusts typecheck dependency sync and updates action versions. |
.github/workflows/publish.yml |
Ensures release quality gate installs needed deps for pyright; updates action versions. |
.github/workflows/pre-release.yml |
Aligns pre-release gate dependency installation with pyright needs; updates action versions. |
.github/workflows/docs.yml |
Updates action versions used for docs deployment workflow. |
.github/workflows/nightly-install.yml |
Updates action versions for nightly install checks. |
.github/workflows/nightly-uat.yml |
Updates action versions for nightly UAT workflow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| - name: Install workspace dependencies | ||
| run: uv sync --all-packages --group dev | ||
| run: uv sync --all-packages --all-extras --group dev | ||
|
|
|
|
||
| - name: Install workspace dependencies | ||
| run: uv sync --all-packages --group dev | ||
| run: uv sync --all-packages --all-extras --group dev | ||
|
|
| with: | ||
| python-version: "3.12" | ||
| - run: uv sync --all-packages | ||
| - run: uv sync --all-packages --all-extras | ||
| - run: cd packages/agent-session-tools && uv run pyright src/ |
Correction to the third follow-up bullet aboveThe first follow-up bullet in the PR description is wrong and should be disregarded. It claimed:
Both halves are incorrect:
The bad inference came from reading What is actually true, and it strengthens the fix
Two consequences:
The other two follow-ups in the description stand: the |
fix(ci): resolve failing typecheck and audit jobs
The release-consistency job runs only check-release-consistency.py, which is stdlib-only (tomllib/re/argparse) and never invokes uv. No uv cache directory is therefore created. setup-uv v9 fails its post step when told to save a cache path that does not exist on disk: Cache path /home/runner/work/_temp/setup-uv-cache does not exist on disk. The check itself passed; only the cleanup step failed, which still failed the job. This surfaced after bumping setup-uv v4 -> v9 in #5. It passed on that PR's run and failed on the post-merge main run, so it is timing-dependent on whether a cache was restored, not deterministic. Set enable-cache: false for this job only. Every other setup-uv job runs uv sync or uv build and legitimately populates the cache.
/api/session/ws now runs the new transport stack — no more
agent_session_manager, no legacy {type:"start"} frame. The route binds
the already-acquired active.ActiveSession to the WS and pumps transport
events both ways under asyncio.TaskGroup + except*.
Why: §1.5 of the agent-session-transport plan. Closes the wiring gap
that left session/transport.py, session/transports/pty.py, and
session/active.py fully implemented but unreachable from the web UI
since PR-B-1..B-4.
Route contract (authoritative for frontend §1.7):
- Preconditions: active session must already be acquired. WS closes
1008 pre-accept when current() is None.
- Origin guard: localhost/127.0.0.1 any port by default, extend via
STUDYLOOP_ALLOWED_ORIGINS env var (plan Blocker B1).
- Session-id match: ?study_session_id must equal active.study_session_id
or close 1008 (pattern from terminal_proxy.py:134).
- Inbound JSON: input/resize/stop control frames.
- Outbound: binary for OutputBytes, text JSON for lifecycle events.
- finally: always await active.release() — guarantees a clean
singleton on disconnect.
Scope cuts (deferred to §1.5b, separate PR):
- POST /session/start still runs legacy tmux+ttyd flow. Re-pointing
it at active.acquire(config, PTYTransport) is the remaining §1.5
work; user deferred because it needs Playwright-driven iteration.
- Frontend WS feature flag (§1.9) becomes a browser concern rather
than server concern — the WS route no longer serves ttyd at all;
iframe path stays behind /terminal/ in terminal_proxy.py.
Legacy test cleanup: 2 stale tests removed from test_web_live_session.py
(test_live_session_websocket_streams_events_and_accepts_input and
test_live_session_websocket_rejects_acp_until_handshake_is_implemented)
— they exercised the now-replaced protocol. test_session_runtime.py's
unit tests of the legacy manager still pass; session_runtime/ stays
on disk until §1.5b also lands.
Tests: 1980 passed, 145 deselected. +8 new in test_web_session_ws.py
(origin guard, session-id mismatch, no-active-session rejection,
Started/Output/Stopped pump, input/resize/stop inbound frames,
release-on-disconnect). Ruff clean, format clean.
Plan: docs/plans/2026-05-09-refactor-agent-session-transport-plan.md
Amendment #5 appended with the full route contract for §1.7 frontend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/session/start now routes through transport=pty by default, calling active.acquire(config, PTYTransport) and returning a ws_url the frontend feeds to /api/session/ws. The legacy tmux+ttyd path is reachable via transport=ttyd in the body or STUDYLOOP_TRANSPORT=ttyd env override (operator kill-switch). Why: closes the §1.5b gap flagged in Amendment #5 — without this bridge, the new WS route was reachable by tests only. Now the full request path works: picker POSTs /session/start → server acquires active session + PTYTransport → returns ws_url → browser opens WS and gets live PTY stream. Transport dispatch: - Body field ``transport: pty|ttyd`` (optional, default pty). - STUDYLOOP_TRANSPORT env wins over body — operator emergency switch per plan §1.9 without touching clients. - On pty path: no tmux check, no ttyd spawn, no tmux metadata in session_state.json. Writes transport=pty into state for clarity. Binary missing (503) now returns a structured payload: {error, agent, binary, install_hint}. Hints cover all five registered agents (claude, codex, gemini, kiro, opencode) so the UI — or an agent orchestrating installs — has a concrete next step. Per-agent "self-heal and retry" is §1.10 follow-up. Adapter shell wrapping: PTYTransport's build_launch_cmd returns ["/bin/sh", "-c", adapter.launch_cmd(...)]. Adapters legitimately use pipes/&& (their launch_cmd returns shell strings), so we execvpe /bin/sh instead of splitting via shlex. Mirrors what tmux send-keys effectively did. Tests: - +5 in test_web_session_start_pty.py: happy path returns ws_url and bypasses tmux, pty-is-default when unset, 409 when active, 503 with install_hint, env=ttyd overrides body=pty (routes to legacy branch and trips the tmux check). - Existing test_web_session.py: 4 legacy guard tests updated to pass transport=ttyd explicitly — they were testing tmux-based semantics that only apply to that branch now. 1985 passed, 145 deselected (was 1980 → +5 net). Ruff + format clean. Plan: Amendment #5 "§1.5b REST→PTY bridge" follow-up is now closed. Amendment #6 (below) documents the contract for §1.7 frontend. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(ci): resolve failing typecheck and audit jobs
The release-consistency job runs only check-release-consistency.py, which is stdlib-only (tomllib/re/argparse) and never invokes uv. No uv cache directory is therefore created. setup-uv v9 fails its post step when told to save a cache path that does not exist on disk: Cache path /home/runner/work/_temp/setup-uv-cache does not exist on disk. The check itself passed; only the cleanup step failed, which still failed the job. This surfaced after bumping setup-uv v4 -> v9 in #5. It passed on that PR's run and failed on the post-merge main run, so it is timing-dependent on whether a cache was restored, not deterministic. Set enable-cache: false for this job only. Every other setup-uv job runs uv sync or uv build and legitimately populates the cache.
/api/session/ws now runs the new transport stack — no more
agent_session_manager, no legacy {type:"start"} frame. The route binds
the already-acquired active.ActiveSession to the WS and pumps transport
events both ways under asyncio.TaskGroup + except*.
Why: §1.5 of the agent-session-transport plan. Closes the wiring gap
that left session/transport.py, session/transports/pty.py, and
session/active.py fully implemented but unreachable from the web UI
since PR-B-1..B-4.
Route contract (authoritative for frontend §1.7):
- Preconditions: active session must already be acquired. WS closes
1008 pre-accept when current() is None.
- Origin guard: localhost/127.0.0.1 any port by default, extend via
STUDYLOOP_ALLOWED_ORIGINS env var (plan Blocker B1).
- Session-id match: ?study_session_id must equal active.study_session_id
or close 1008 (pattern from terminal_proxy.py:134).
- Inbound JSON: input/resize/stop control frames.
- Outbound: binary for OutputBytes, text JSON for lifecycle events.
- finally: always await active.release() — guarantees a clean
singleton on disconnect.
Scope cuts (deferred to §1.5b, separate PR):
- POST /session/start still runs legacy tmux+ttyd flow. Re-pointing
it at active.acquire(config, PTYTransport) is the remaining §1.5
work; user deferred because it needs Playwright-driven iteration.
- Frontend WS feature flag (§1.9) becomes a browser concern rather
than server concern — the WS route no longer serves ttyd at all;
iframe path stays behind /terminal/ in terminal_proxy.py.
Legacy test cleanup: 2 stale tests removed from test_web_live_session.py
(test_live_session_websocket_streams_events_and_accepts_input and
test_live_session_websocket_rejects_acp_until_handshake_is_implemented)
— they exercised the now-replaced protocol. test_session_runtime.py's
unit tests of the legacy manager still pass; session_runtime/ stays
on disk until §1.5b also lands.
Tests: 1980 passed, 145 deselected. +8 new in test_web_session_ws.py
(origin guard, session-id mismatch, no-active-session rejection,
Started/Output/Stopped pump, input/resize/stop inbound frames,
release-on-disconnect). Ruff clean, format clean.
Plan: docs/plans/2026-05-09-refactor-agent-session-transport-plan.md
Amendment #5 appended with the full route contract for §1.7 frontend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/session/start now routes through transport=pty by default, calling active.acquire(config, PTYTransport) and returning a ws_url the frontend feeds to /api/session/ws. The legacy tmux+ttyd path is reachable via transport=ttyd in the body or STUDYLOOP_TRANSPORT=ttyd env override (operator kill-switch). Why: closes the §1.5b gap flagged in Amendment #5 — without this bridge, the new WS route was reachable by tests only. Now the full request path works: picker POSTs /session/start → server acquires active session + PTYTransport → returns ws_url → browser opens WS and gets live PTY stream. Transport dispatch: - Body field ``transport: pty|ttyd`` (optional, default pty). - STUDYLOOP_TRANSPORT env wins over body — operator emergency switch per plan §1.9 without touching clients. - On pty path: no tmux check, no ttyd spawn, no tmux metadata in session_state.json. Writes transport=pty into state for clarity. Binary missing (503) now returns a structured payload: {error, agent, binary, install_hint}. Hints cover all five registered agents (claude, codex, gemini, kiro, opencode) so the UI — or an agent orchestrating installs — has a concrete next step. Per-agent "self-heal and retry" is §1.10 follow-up. Adapter shell wrapping: PTYTransport's build_launch_cmd returns ["/bin/sh", "-c", adapter.launch_cmd(...)]. Adapters legitimately use pipes/&& (their launch_cmd returns shell strings), so we execvpe /bin/sh instead of splitting via shlex. Mirrors what tmux send-keys effectively did. Tests: - +5 in test_web_session_start_pty.py: happy path returns ws_url and bypasses tmux, pty-is-default when unset, 409 when active, 503 with install_hint, env=ttyd overrides body=pty (routes to legacy branch and trips the tmux check). - Existing test_web_session.py: 4 legacy guard tests updated to pass transport=ttyd explicitly — they were testing tmux-based semantics that only apply to that branch now. 1985 passed, 145 deselected (was 1980 → +5 net). Ruff + format clean. Plan: Amendment #5 "§1.5b REST→PTY bridge" follow-up is now closed. Amendment #6 (below) documents the contract for §1.7 frontend. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/api/session/ws now runs the new transport stack — no more
agent_session_manager, no legacy {type:"start"} frame. The route binds
the already-acquired active.ActiveSession to the WS and pumps transport
events both ways under asyncio.TaskGroup + except*.
Why: §1.5 of the agent-session-transport plan. Closes the wiring gap
that left session/transport.py, session/transports/pty.py, and
session/active.py fully implemented but unreachable from the web UI
since PR-B-1..B-4.
Route contract (authoritative for frontend §1.7):
- Preconditions: active session must already be acquired. WS closes
1008 pre-accept when current() is None.
- Origin guard: localhost/127.0.0.1 any port by default, extend via
STUDYLOOP_ALLOWED_ORIGINS env var (plan Blocker B1).
- Session-id match: ?study_session_id must equal active.study_session_id
or close 1008 (pattern from terminal_proxy.py:134).
- Inbound JSON: input/resize/stop control frames.
- Outbound: binary for OutputBytes, text JSON for lifecycle events.
- finally: always await active.release() — guarantees a clean
singleton on disconnect.
Scope cuts (deferred to §1.5b, separate PR):
- POST /session/start still runs legacy tmux+ttyd flow. Re-pointing
it at active.acquire(config, PTYTransport) is the remaining §1.5
work; user deferred because it needs Playwright-driven iteration.
- Frontend WS feature flag (§1.9) becomes a browser concern rather
than server concern — the WS route no longer serves ttyd at all;
iframe path stays behind /terminal/ in terminal_proxy.py.
Legacy test cleanup: 2 stale tests removed from test_web_live_session.py
(test_live_session_websocket_streams_events_and_accepts_input and
test_live_session_websocket_rejects_acp_until_handshake_is_implemented)
— they exercised the now-replaced protocol. test_session_runtime.py's
unit tests of the legacy manager still pass; session_runtime/ stays
on disk until §1.5b also lands.
Tests: 1980 passed, 145 deselected. +8 new in test_web_session_ws.py
(origin guard, session-id mismatch, no-active-session rejection,
Started/Output/Stopped pump, input/resize/stop inbound frames,
release-on-disconnect). Ruff clean, format clean.
Plan: docs/plans/2026-05-09-refactor-agent-session-transport-plan.md
Amendment #5 appended with the full route contract for §1.7 frontend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/session/start now routes through transport=pty by default, calling active.acquire(config, PTYTransport) and returning a ws_url the frontend feeds to /api/session/ws. The legacy tmux+ttyd path is reachable via transport=ttyd in the body or STUDYLOOP_TRANSPORT=ttyd env override (operator kill-switch). Why: closes the §1.5b gap flagged in Amendment #5 — without this bridge, the new WS route was reachable by tests only. Now the full request path works: picker POSTs /session/start → server acquires active session + PTYTransport → returns ws_url → browser opens WS and gets live PTY stream. Transport dispatch: - Body field ``transport: pty|ttyd`` (optional, default pty). - STUDYLOOP_TRANSPORT env wins over body — operator emergency switch per plan §1.9 without touching clients. - On pty path: no tmux check, no ttyd spawn, no tmux metadata in session_state.json. Writes transport=pty into state for clarity. Binary missing (503) now returns a structured payload: {error, agent, binary, install_hint}. Hints cover all five registered agents (claude, codex, gemini, kiro, opencode) so the UI — or an agent orchestrating installs — has a concrete next step. Per-agent "self-heal and retry" is §1.10 follow-up. Adapter shell wrapping: PTYTransport's build_launch_cmd returns ["/bin/sh", "-c", adapter.launch_cmd(...)]. Adapters legitimately use pipes/&& (their launch_cmd returns shell strings), so we execvpe /bin/sh instead of splitting via shlex. Mirrors what tmux send-keys effectively did. Tests: - +5 in test_web_session_start_pty.py: happy path returns ws_url and bypasses tmux, pty-is-default when unset, 409 when active, 503 with install_hint, env=ttyd overrides body=pty (routes to legacy branch and trips the tmux check). - Existing test_web_session.py: 4 legacy guard tests updated to pass transport=ttyd explicitly — they were testing tmux-based semantics that only apply to that branch now. 1985 passed, 145 deselected (was 1980 → +5 net). Ruff + format clean. Plan: Amendment #5 "§1.5b REST→PTY bridge" follow-up is now closed. Amendment #6 (below) documents the contract for §1.7 frontend. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(ci): resolve failing typecheck and audit jobs
The release-consistency job runs only check-release-consistency.py, which is stdlib-only (tomllib/re/argparse) and never invokes uv. No uv cache directory is therefore created. setup-uv v9 fails its post step when told to save a cache path that does not exist on disk: Cache path /home/runner/work/_temp/setup-uv-cache does not exist on disk. The check itself passed; only the cleanup step failed, which still failed the job. This surfaced after bumping setup-uv v4 -> v9 in #5. It passed on that PR's run and failed on the post-merge main run, so it is timing-dependent on whether a cache was restored, not deterministic. Set enable-cache: false for this job only. Every other setup-uv job runs uv sync or uv build and legitimately populates the cache.
fix(ci): resolve failing typecheck and audit jobs
The release-consistency job runs only check-release-consistency.py, which is stdlib-only (tomllib/re/argparse) and never invokes uv. No uv cache directory is therefore created. setup-uv v9 fails its post step when told to save a cache path that does not exist on disk: Cache path /home/runner/work/_temp/setup-uv-cache does not exist on disk. The check itself passed; only the cleanup step failed, which still failed the job. This surfaced after bumping setup-uv v4 -> v9 in #5. It passed on that PR's run and failed on the post-merge main run, so it is timing-dependent on whether a cache was restored, not deterministic. Set enable-cache: false for this job only. Every other setup-uv job runs uv sync or uv build and legitimately populates the cache.
/api/session/ws now runs the new transport stack — no more
agent_session_manager, no legacy {type:"start"} frame. The route binds
the already-acquired active.ActiveSession to the WS and pumps transport
events both ways under asyncio.TaskGroup + except*.
Why: §1.5 of the agent-session-transport plan. Closes the wiring gap
that left session/transport.py, session/transports/pty.py, and
session/active.py fully implemented but unreachable from the web UI
since PR-B-1..B-4.
Route contract (authoritative for frontend §1.7):
- Preconditions: active session must already be acquired. WS closes
1008 pre-accept when current() is None.
- Origin guard: localhost/127.0.0.1 any port by default, extend via
STUDYLOOP_ALLOWED_ORIGINS env var (plan Blocker B1).
- Session-id match: ?study_session_id must equal active.study_session_id
or close 1008 (pattern from terminal_proxy.py:134).
- Inbound JSON: input/resize/stop control frames.
- Outbound: binary for OutputBytes, text JSON for lifecycle events.
- finally: always await active.release() — guarantees a clean
singleton on disconnect.
Scope cuts (deferred to §1.5b, separate PR):
- POST /session/start still runs legacy tmux+ttyd flow. Re-pointing
it at active.acquire(config, PTYTransport) is the remaining §1.5
work; user deferred because it needs Playwright-driven iteration.
- Frontend WS feature flag (§1.9) becomes a browser concern rather
than server concern — the WS route no longer serves ttyd at all;
iframe path stays behind /terminal/ in terminal_proxy.py.
Legacy test cleanup: 2 stale tests removed from test_web_live_session.py
(test_live_session_websocket_streams_events_and_accepts_input and
test_live_session_websocket_rejects_acp_until_handshake_is_implemented)
— they exercised the now-replaced protocol. test_session_runtime.py's
unit tests of the legacy manager still pass; session_runtime/ stays
on disk until §1.5b also lands.
Tests: 1980 passed, 145 deselected. +8 new in test_web_session_ws.py
(origin guard, session-id mismatch, no-active-session rejection,
Started/Output/Stopped pump, input/resize/stop inbound frames,
release-on-disconnect). Ruff clean, format clean.
Plan: docs/plans/2026-05-09-refactor-agent-session-transport-plan.md
Amendment #5 appended with the full route contract for §1.7 frontend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/session/start now routes through transport=pty by default, calling active.acquire(config, PTYTransport) and returning a ws_url the frontend feeds to /api/session/ws. The legacy tmux+ttyd path is reachable via transport=ttyd in the body or STUDYLOOP_TRANSPORT=ttyd env override (operator kill-switch). Why: closes the §1.5b gap flagged in Amendment #5 — without this bridge, the new WS route was reachable by tests only. Now the full request path works: picker POSTs /session/start → server acquires active session + PTYTransport → returns ws_url → browser opens WS and gets live PTY stream. Transport dispatch: - Body field ``transport: pty|ttyd`` (optional, default pty). - STUDYLOOP_TRANSPORT env wins over body — operator emergency switch per plan §1.9 without touching clients. - On pty path: no tmux check, no ttyd spawn, no tmux metadata in session_state.json. Writes transport=pty into state for clarity. Binary missing (503) now returns a structured payload: {error, agent, binary, install_hint}. Hints cover all five registered agents (claude, codex, gemini, kiro, opencode) so the UI — or an agent orchestrating installs — has a concrete next step. Per-agent "self-heal and retry" is §1.10 follow-up. Adapter shell wrapping: PTYTransport's build_launch_cmd returns ["/bin/sh", "-c", adapter.launch_cmd(...)]. Adapters legitimately use pipes/&& (their launch_cmd returns shell strings), so we execvpe /bin/sh instead of splitting via shlex. Mirrors what tmux send-keys effectively did. Tests: - +5 in test_web_session_start_pty.py: happy path returns ws_url and bypasses tmux, pty-is-default when unset, 409 when active, 503 with install_hint, env=ttyd overrides body=pty (routes to legacy branch and trips the tmux check). - Existing test_web_session.py: 4 legacy guard tests updated to pass transport=ttyd explicitly — they were testing tmux-based semantics that only apply to that branch now. 1985 passed, 145 deselected (was 1980 → +5 net). Ruff + format clean. Plan: Amendment #5 "§1.5b REST→PTY bridge" follow-up is now closed. Amendment #6 (below) documents the contract for §1.7 frontend. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(ci): resolve failing typecheck and audit jobs
The release-consistency job runs only check-release-consistency.py, which is stdlib-only (tomllib/re/argparse) and never invokes uv. No uv cache directory is therefore created. setup-uv v9 fails its post step when told to save a cache path that does not exist on disk: Cache path /home/runner/work/_temp/setup-uv-cache does not exist on disk. The check itself passed; only the cleanup step failed, which still failed the job. This surfaced after bumping setup-uv v4 -> v9 in #5. It passed on that PR's run and failed on the post-merge main run, so it is timing-dependent on whether a cache was restored, not deterministic. Set enable-cache: false for this job only. Every other setup-uv job runs uv sync or uv build and legitimately populates the cache.
/api/session/ws now runs the new transport stack — no more
agent_session_manager, no legacy {type:"start"} frame. The route binds
the already-acquired active.ActiveSession to the WS and pumps transport
events both ways under asyncio.TaskGroup + except*.
Why: §1.5 of the agent-session-transport plan. Closes the wiring gap
that left session/transport.py, session/transports/pty.py, and
session/active.py fully implemented but unreachable from the web UI
since PR-B-1..B-4.
Route contract (authoritative for frontend §1.7):
- Preconditions: active session must already be acquired. WS closes
1008 pre-accept when current() is None.
- Origin guard: localhost/127.0.0.1 any port by default, extend via
STUDYLOOP_ALLOWED_ORIGINS env var (plan Blocker B1).
- Session-id match: ?study_session_id must equal active.study_session_id
or close 1008 (pattern from terminal_proxy.py:134).
- Inbound JSON: input/resize/stop control frames.
- Outbound: binary for OutputBytes, text JSON for lifecycle events.
- finally: always await active.release() — guarantees a clean
singleton on disconnect.
Scope cuts (deferred to §1.5b, separate PR):
- POST /session/start still runs legacy tmux+ttyd flow. Re-pointing
it at active.acquire(config, PTYTransport) is the remaining §1.5
work; user deferred because it needs Playwright-driven iteration.
- Frontend WS feature flag (§1.9) becomes a browser concern rather
than server concern — the WS route no longer serves ttyd at all;
iframe path stays behind /terminal/ in terminal_proxy.py.
Legacy test cleanup: 2 stale tests removed from test_web_live_session.py
(test_live_session_websocket_streams_events_and_accepts_input and
test_live_session_websocket_rejects_acp_until_handshake_is_implemented)
— they exercised the now-replaced protocol. test_session_runtime.py's
unit tests of the legacy manager still pass; session_runtime/ stays
on disk until §1.5b also lands.
Tests: 1980 passed, 145 deselected. +8 new in test_web_session_ws.py
(origin guard, session-id mismatch, no-active-session rejection,
Started/Output/Stopped pump, input/resize/stop inbound frames,
release-on-disconnect). Ruff clean, format clean.
Plan: docs/plans/2026-05-09-refactor-agent-session-transport-plan.md
Amendment #5 appended with the full route contract for §1.7 frontend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/session/start now routes through transport=pty by default, calling active.acquire(config, PTYTransport) and returning a ws_url the frontend feeds to /api/session/ws. The legacy tmux+ttyd path is reachable via transport=ttyd in the body or STUDYLOOP_TRANSPORT=ttyd env override (operator kill-switch). Why: closes the §1.5b gap flagged in Amendment #5 — without this bridge, the new WS route was reachable by tests only. Now the full request path works: picker POSTs /session/start → server acquires active session + PTYTransport → returns ws_url → browser opens WS and gets live PTY stream. Transport dispatch: - Body field ``transport: pty|ttyd`` (optional, default pty). - STUDYLOOP_TRANSPORT env wins over body — operator emergency switch per plan §1.9 without touching clients. - On pty path: no tmux check, no ttyd spawn, no tmux metadata in session_state.json. Writes transport=pty into state for clarity. Binary missing (503) now returns a structured payload: {error, agent, binary, install_hint}. Hints cover all five registered agents (claude, codex, gemini, kiro, opencode) so the UI — or an agent orchestrating installs — has a concrete next step. Per-agent "self-heal and retry" is §1.10 follow-up. Adapter shell wrapping: PTYTransport's build_launch_cmd returns ["/bin/sh", "-c", adapter.launch_cmd(...)]. Adapters legitimately use pipes/&& (their launch_cmd returns shell strings), so we execvpe /bin/sh instead of splitting via shlex. Mirrors what tmux send-keys effectively did. Tests: - +5 in test_web_session_start_pty.py: happy path returns ws_url and bypasses tmux, pty-is-default when unset, 409 when active, 503 with install_hint, env=ttyd overrides body=pty (routes to legacy branch and trips the tmux check). - Existing test_web_session.py: 4 legacy guard tests updated to pass transport=ttyd explicitly — they were testing tmux-based semantics that only apply to that branch now. 1985 passed, 145 deselected (was 1980 → +5 net). Ruff + format clean. Plan: Amendment #5 "§1.5b REST→PTY bridge" follow-up is now closed. Amendment #6 (below) documents the contract for §1.7 frontend. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
fix(ci): resolve failing typecheck and audit jobs
The release-consistency job runs only check-release-consistency.py, which is stdlib-only (tomllib/re/argparse) and never invokes uv. No uv cache directory is therefore created. setup-uv v9 fails its post step when told to save a cache path that does not exist on disk: Cache path /home/runner/work/_temp/setup-uv-cache does not exist on disk. The check itself passed; only the cleanup step failed, which still failed the job. This surfaced after bumping setup-uv v4 -> v9 in #5. It passed on that PR's run and failed on the post-merge main run, so it is timing-dependent on whether a cache was restored, not deterministic. Set enable-cache: false for this job only. Every other setup-uv job runs uv sync or uv build and legitimately populates the cache.
/api/session/ws now runs the new transport stack — no more
agent_session_manager, no legacy {type:"start"} frame. The route binds
the already-acquired active.ActiveSession to the WS and pumps transport
events both ways under asyncio.TaskGroup + except*.
Why: §1.5 of the agent-session-transport plan. Closes the wiring gap
that left session/transport.py, session/transports/pty.py, and
session/active.py fully implemented but unreachable from the web UI
since PR-B-1..B-4.
Route contract (authoritative for frontend §1.7):
- Preconditions: active session must already be acquired. WS closes
1008 pre-accept when current() is None.
- Origin guard: localhost/127.0.0.1 any port by default, extend via
STUDYLOOP_ALLOWED_ORIGINS env var (plan Blocker B1).
- Session-id match: ?study_session_id must equal active.study_session_id
or close 1008 (pattern from terminal_proxy.py:134).
- Inbound JSON: input/resize/stop control frames.
- Outbound: binary for OutputBytes, text JSON for lifecycle events.
- finally: always await active.release() — guarantees a clean
singleton on disconnect.
Scope cuts (deferred to §1.5b, separate PR):
- POST /session/start still runs legacy tmux+ttyd flow. Re-pointing
it at active.acquire(config, PTYTransport) is the remaining §1.5
work; user deferred because it needs Playwright-driven iteration.
- Frontend WS feature flag (§1.9) becomes a browser concern rather
than server concern — the WS route no longer serves ttyd at all;
iframe path stays behind /terminal/ in terminal_proxy.py.
Legacy test cleanup: 2 stale tests removed from test_web_live_session.py
(test_live_session_websocket_streams_events_and_accepts_input and
test_live_session_websocket_rejects_acp_until_handshake_is_implemented)
— they exercised the now-replaced protocol. test_session_runtime.py's
unit tests of the legacy manager still pass; session_runtime/ stays
on disk until §1.5b also lands.
Tests: 1980 passed, 145 deselected. +8 new in test_web_session_ws.py
(origin guard, session-id mismatch, no-active-session rejection,
Started/Output/Stopped pump, input/resize/stop inbound frames,
release-on-disconnect). Ruff clean, format clean.
Plan: docs/plans/2026-05-09-refactor-agent-session-transport-plan.md
Amendment #5 appended with the full route contract for §1.7 frontend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/session/start now routes through transport=pty by default, calling active.acquire(config, PTYTransport) and returning a ws_url the frontend feeds to /api/session/ws. The legacy tmux+ttyd path is reachable via transport=ttyd in the body or STUDYLOOP_TRANSPORT=ttyd env override (operator kill-switch). Why: closes the §1.5b gap flagged in Amendment #5 — without this bridge, the new WS route was reachable by tests only. Now the full request path works: picker POSTs /session/start → server acquires active session + PTYTransport → returns ws_url → browser opens WS and gets live PTY stream. Transport dispatch: - Body field ``transport: pty|ttyd`` (optional, default pty). - STUDYLOOP_TRANSPORT env wins over body — operator emergency switch per plan §1.9 without touching clients. - On pty path: no tmux check, no ttyd spawn, no tmux metadata in session_state.json. Writes transport=pty into state for clarity. Binary missing (503) now returns a structured payload: {error, agent, binary, install_hint}. Hints cover all five registered agents (claude, codex, gemini, kiro, opencode) so the UI — or an agent orchestrating installs — has a concrete next step. Per-agent "self-heal and retry" is §1.10 follow-up. Adapter shell wrapping: PTYTransport's build_launch_cmd returns ["/bin/sh", "-c", adapter.launch_cmd(...)]. Adapters legitimately use pipes/&& (their launch_cmd returns shell strings), so we execvpe /bin/sh instead of splitting via shlex. Mirrors what tmux send-keys effectively did. Tests: - +5 in test_web_session_start_pty.py: happy path returns ws_url and bypasses tmux, pty-is-default when unset, 409 when active, 503 with install_hint, env=ttyd overrides body=pty (routes to legacy branch and trips the tmux check). - Existing test_web_session.py: 4 legacy guard tests updated to pass transport=ttyd explicitly — they were testing tmux-based semantics that only apply to that branch now. 1985 passed, 145 deselected (was 1980 → +5 net). Ruff + format clean. Plan: Amendment #5 "§1.5b REST→PTY bridge" follow-up is now closed. Amendment #6 (below) documents the contract for §1.7 frontend. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/api/session/ws now runs the new transport stack — no more
agent_session_manager, no legacy {type:"start"} frame. The route binds
the already-acquired active.ActiveSession to the WS and pumps transport
events both ways under asyncio.TaskGroup + except*.
Why: §1.5 of the agent-session-transport plan. Closes the wiring gap
that left session/transport.py, session/transports/pty.py, and
session/active.py fully implemented but unreachable from the web UI
since PR-B-1..B-4.
Route contract (authoritative for frontend §1.7):
- Preconditions: active session must already be acquired. WS closes
1008 pre-accept when current() is None.
- Origin guard: localhost/127.0.0.1 any port by default, extend via
STUDYLOOP_ALLOWED_ORIGINS env var (plan Blocker B1).
- Session-id match: ?study_session_id must equal active.study_session_id
or close 1008 (pattern from terminal_proxy.py:134).
- Inbound JSON: input/resize/stop control frames.
- Outbound: binary for OutputBytes, text JSON for lifecycle events.
- finally: always await active.release() — guarantees a clean
singleton on disconnect.
Scope cuts (deferred to §1.5b, separate PR):
- POST /session/start still runs legacy tmux+ttyd flow. Re-pointing
it at active.acquire(config, PTYTransport) is the remaining §1.5
work; user deferred because it needs Playwright-driven iteration.
- Frontend WS feature flag (§1.9) becomes a browser concern rather
than server concern — the WS route no longer serves ttyd at all;
iframe path stays behind /terminal/ in terminal_proxy.py.
Legacy test cleanup: 2 stale tests removed from test_web_live_session.py
(test_live_session_websocket_streams_events_and_accepts_input and
test_live_session_websocket_rejects_acp_until_handshake_is_implemented)
— they exercised the now-replaced protocol. test_session_runtime.py's
unit tests of the legacy manager still pass; session_runtime/ stays
on disk until §1.5b also lands.
Tests: 1980 passed, 145 deselected. +8 new in test_web_session_ws.py
(origin guard, session-id mismatch, no-active-session rejection,
Started/Output/Stopped pump, input/resize/stop inbound frames,
release-on-disconnect). Ruff clean, format clean.
Plan: docs/plans/2026-05-09-refactor-agent-session-transport-plan.md
Amendment #5 appended with the full route contract for §1.7 frontend.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
POST /api/session/start now routes through transport=pty by default, calling active.acquire(config, PTYTransport) and returning a ws_url the frontend feeds to /api/session/ws. The legacy tmux+ttyd path is reachable via transport=ttyd in the body or STUDYLOOP_TRANSPORT=ttyd env override (operator kill-switch). Why: closes the §1.5b gap flagged in Amendment #5 — without this bridge, the new WS route was reachable by tests only. Now the full request path works: picker POSTs /session/start → server acquires active session + PTYTransport → returns ws_url → browser opens WS and gets live PTY stream. Transport dispatch: - Body field ``transport: pty|ttyd`` (optional, default pty). - STUDYLOOP_TRANSPORT env wins over body — operator emergency switch per plan §1.9 without touching clients. - On pty path: no tmux check, no ttyd spawn, no tmux metadata in session_state.json. Writes transport=pty into state for clarity. Binary missing (503) now returns a structured payload: {error, agent, binary, install_hint}. Hints cover all five registered agents (claude, codex, gemini, kiro, opencode) so the UI — or an agent orchestrating installs — has a concrete next step. Per-agent "self-heal and retry" is §1.10 follow-up. Adapter shell wrapping: PTYTransport's build_launch_cmd returns ["/bin/sh", "-c", adapter.launch_cmd(...)]. Adapters legitimately use pipes/&& (their launch_cmd returns shell strings), so we execvpe /bin/sh instead of splitting via shlex. Mirrors what tmux send-keys effectively did. Tests: - +5 in test_web_session_start_pty.py: happy path returns ws_url and bypasses tmux, pty-is-default when unset, 409 when active, 503 with install_hint, env=ttyd overrides body=pty (routes to legacy branch and trips the tmux check). - Existing test_web_session.py: 4 legacy guard tests updated to pass transport=ttyd explicitly — they were testing tmux-based semantics that only apply to that branch now. 1985 passed, 145 deselected (was 1980 → +5 net). Ruff + format clean. Plan: Amendment #5 "§1.5b REST→PTY bridge" follow-up is now closed. Amendment #6 (below) documents the contract for §1.7 frontend. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes both failing jobs on
main(run 29416495485).typecheck— 21reportMissingImportserrorsuv sync --all-packagesinstalls each workspace package's default dependencies but not its extras.studyctlkeepsfastapiin thewebextra, so pyright could not resolvefastapianywhere undersrc/studyctl/web/.Added
--all-extrasto the sync step that precedes pyright. With extras installed, pyright reports 0 errors for both packages — the web code typechecks cleanly, so no suppressions were needed.publish.ymlandpre-release.ymlran pyright after the same incomplete sync and carried the identical latent defect. They are fixed too, otherwise the next release would have failed the same way.audit— 65 advisoriesuv auditfailed on 65 advisories across 15 locked packages. Upgraded only those packages, deliberately notuv lock --upgrade: a blanket upgrade also bumps ruff, which then reports 124 new lint errors — trading a redauditjob for a redlintjob.uv auditnow reports no known vulnerabilities in 194 packages.Node 20 deprecation (bonus)
actions/checkoutv4.2.2 -> v7.0.1 andastral-sh/setup-uvv4 -> v9.0.0 across all workflows. Both targeted Node 20 and were already being force-run on Node 24; this clears the warning before it becomes an error. Existing pin style preserved per file. Reviewed release notes: checkout v5-v7 and setup-uv v9 have no breaking changes affecting this repo's usage.Verification
Every CI job run locally against this branch:
starlettecrosses a major version (0.52.1 -> 1.3.1), which CI's test selection does not cover because the web tests aree2e-marked. Verified separately: app constructs with 23 routes and/api/courses,/api/history,/,/openapi.jsonall return 200 under starlette 1.x. The 3 Playwright failures intest_web_terminal.pyreproduce identically on the pre-upgrade lock, so they are pre-existing and unrelated.Follow-ups not included
nightly-install.yml,nightly-uat.ymlandpre-release.ymlpass--extra dev --extra test, butstudyctldefines no such extras (devis a dependency group).uvsilently ignores unknown extras and exits 0, so those jobs are quietly installing less than intended.testjob does not install extras, sosrc/studyctl/web/is typechecked but never exercised by unit tests.audithard-failsmainon any new advisory in any transitive dependency, somaincan go red without a code change. Worth considering a scheduled run or a documented triage path.