feat: merge study command + E2E experience tests + bridge CLI - #2
Merged
Conversation
…or tests - Delete TUI dashboard, scheduler, calendar, state CLI - Prune CLI: remove wins/streaks/resume/progress-map/bridges/teachback/schedule - Archive Crush, Amp, and 5 extra Claude agent definitions - Fix doctor integration tests: block network in autouse fixture - Restore state.py (needed for NotebookLM SyncState, not push/pull CLI) - Rewrite README: 426 → 162 lines, focused on 4 core use cases - All tests green: 612 passed (247 studyctl + 357 session-tools + 8 doctor)
- cli-reference.md: rewritten to reflect 13 commands (removed TUI, schedule, state, wins, streaks, bridges, teachback) - setup-guide.md: removed TUI section, replaced state push/pull with session-sync - roadmap.md: added compaction notice at top - TODO.md: rewritten with archived features section - session-protocol.md: replaced studyctl wins/schedule-blocks references - audhd-framework.md: replaced studyctl wins reference - audhd-learning-philosophy.md: replaced studyctl bridge add reference
…CLI, IPC protocol Phase 1 of the live study session dashboard. Closes the critical gap where parking lot (AuDHD rabbit-hole prevention) had zero DB backing despite being documented in 4 agent framework files. New modules: - parking.py: 6 CRUD functions for parked_topics table (park, get, schedule, resolve, dismiss) - session_state.py: IPC file read/write/parse for dashboard viewports (session-state.json, session-topics.md, session-parking.md) - cli/_session.py: session start/end/status and park commands Changes: - Migration v14: parked_topics table with status lifecycle (pending → scheduled → resolved/dismissed) - Re-added resume, streaks, wins, progress commands from main - Agent protocols updated with Session File Protocol instructions (Claude, Gemini, OpenCode, shared session-protocol.md) Tests: 26 new tests (9 parking, 12 session_state, 5 CLI session) Full suite: 638 passed, 5 skipped Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
cmux MCP server (cmuxlayer) added to Claude, Gemini, and OpenCode agent configs. New "cmux Dashboard Protocol" section in session-protocol.md with complete MCP tool call sequences for: - Session start: split pane, sidebar status/progress, tab rename - During session: topic updates, counter increments, timer progression, break notifications - Session end: summary view, progress complete marker The cmux viewport is additive — file-IPC writes continue unconditionally. If cmux MCP tools are unavailable (non-macOS, not in cmux terminal), the agent skips cmux calls silently and falls back to file-only IPC. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Design decisions and rationale for the live study session dashboard: agent-initiated sessions (no start question), combined activity pane with color+shape accessibility, energy-adaptive timer from break-science.md, file-IPC protocol, 3-viewport architecture (cmux, web PWA, TUI). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SSE-powered live session dashboard at /session with HTMX + Alpine.js. The agent writes to IPC files during a study session; the web app polls them via SSE and pushes HTML fragments with OOB swaps for real-time updates without page reloads. New: - web/routes/session.py: SSE stream (/api/session/stream) + JSON state endpoint (/api/session/state), 5 HTML rendering helpers with XSS-safe escaping, shapes+colors for accessibility (✓ ★ ◆ ▲ ○) - web/static/session.html: Dashboard page with HTMX SSE extension, Alpine.js energy-adaptive timer (thresholds from break-science.md), activity feed, counter bar (WINS|PARKED|REVIEW), summary view - Session indicator banner on courses page when a session is active - Service worker v2: skip caching SSE streams and HTMX fragments Tests: 14 new tests (page serving, API state, SSE format, render functions for activity feed, counters, summary, XSS escaping) Full suite: 677 passed, 3 skipped Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes from live testing: 1. parking.py: _connect() now checks if parked_topics table exists and runs agent-session-tools migrations automatically if missing. Previously park_topic() would fail with "no such table" on databases that hadn't been migrated to v14. 2. session.html: Added pause/resume and reset buttons to the session timer. Pause shifts startTime forward on resume so elapsed time only counts active study time. Reset starts fresh with threshold messages cleared. "Paused" label shown in amber when timer is paused. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Architecture decisions for the next evolution of studyctl: - tmux as session runtime, all viewports read from it - `studyctl study` as universal entry point (CLI, /studyctl, @studyctl) - One layout all modes (study, co-study), mode changes agent behaviour - Two-port model: dashboard (read-only, safe) + ttyd terminal (auth'd) - Always-on server: start on Mac Mini, continue from any device - Timer: elapsed + pomodoro, both pausable, energy-adaptive thresholds - Speech-to-text recommendations (Warp, Handy, Wispr Flow) - UX: session resume, quick capture, "I'm stuck" buttons, energy tracking Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents 5 false assumptions caught by 9 research agents before any code was written: 1. ttyd TTYD_CREDENTIAL env var does not exist — use nginx proxy 2. age CLI requires TTY — use pyrage + macOS Keychain instead 3. Only Claude supports --system-prompt-file — others need config files 4. tmux -f is server-level, ignored on running server — use source-file 5. Distributed timer needs reconciliation — stateless computation simpler Includes prevention protocol: treat every CLI capability in a plan as an untested hypothesis, verify against actual binaries before coding. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SSE mtime optimization: replace JSON hash with 3 stat() calls for O(1) change detection in session stream endpoint - Parking dedup: migration v15 adds unique index on (study_session_id, question), INSERT OR IGNORE prevents session_end re-inserting entries - IPC file permissions: 0700 on session dir, 0600 on all IPC files via os.open() with explicit mode bits - CORS wildcard removed from stdlib web server - Docs: README architecture diagram, CLI reference, and roadmap updated for live session dashboard features (session CLI, SSE, parking lot) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New `studyctl study` command creates a complete tmux-based study environment with one command: agent pane + Textual sidebar + optional web dashboard. New modules: - tmux.py: thin wrapper with -P -F pane IDs, source-file config, os.execvp attach, file lock for creation, version detection - agent_launcher.py: detect installed agents, build persona files with mkstemp 0600, Claude-only initially (dict makes adding trivial) - tui/sidebar.py: Textual app with TimerWidget (energy-adaptive colour phases from break-science.md), ActivityFeed, CounterBar, IPC polling via worker thread, session-oneline.txt for tmux status bar - cli/_study.py: orchestrator — session start, tmux layout, agent launch, sidebar launch, --resume, --end, --web, --mode co-study Also: - Agent persona files (study.md, co-study.md) in agents/shared/personas/ - 39 new tests (test_tmux, test_agent_launcher, test_study) - 359 studyctl tests pass, 3 skipped Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…mmand - README: replace session start/end with studyctl study as primary entry - CLI reference: add full studyctl study section with modes, flags, and architecture description; demote session commands to "low-level" - Roadmap: mark Phase 1 Unified Session as complete Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three fixes from live testing: - Auto-switch when already in tmux: use switch-client instead of printing a hint. Covers both study start and --resume paths. - Sidebar launch: use sys.executable + python -m studyctl.tui.sidebar instead of uv run (which needs a project dir). Added __main__.py entry points for both studyctl.tui and studyctl.tui.sidebar. - Split pane: use percentage (25%) not fixed columns (35) so the sidebar adapts to terminal width. Also: - Bundled catppuccin-inspired tmux config at data/tmux-studyctl.conf (loaded via source-file, user override at ~/.config/studyctl/) - Added switch_client() to tmux.py wrapper - Quick park popup: prefix+p opens display-popup Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…imensions Root cause: create_session used -x 200 -y 50, so the 25% split was calculated at 200 columns. When the client switched/attached, tmux resized but the split ratio was distorted (sidebar became a sliver). Fix: - Remove -x/-y from create_session — let session use default size - Switch client BEFORE splitting when already in tmux, so the split percentage is calculated against the actual terminal width - Update test to match new code path (no console output in tmux path) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
`from textual.worker import work` fails on Textual 8.x. The correct import is `from textual import work` (re-exported at package level). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two improvements from live testing: - Pass command= to create_session and split_pane instead of send_keys. Commands run directly in the pane — no shell prompt, no visible command text in scrollback. Main pane shows only Claude Code output. - Trim tmux-studyctl.conf to minimal overlay: only status-right (oneline readout) and prefix+p (quick park popup). Does not override user's prefix, keybindings, theme, colours, or mouse settings. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The bundled tmux-studyctl.conf overrides status-right (clobbering catppuccin's generated output) and binds prefix+p (conflicting with floax). Only load a user's explicit override at ~/.config/studyctl/tmux-studyctl.conf. The bundled file remains as a reference template in data/. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Non-technical users shouldn't need to know about tmux or IPC files. When the agent process exits (user quits Claude, /exit, or Ctrl+C), a wrapper command automatically: - Ends the DB session - Cleans up persona + IPC files - Switches tmux client back to the previous session - Kills the study tmux session Also: - Sidebar Q (uppercase) triggers full session end from the sidebar - studyctl study --end now switches back to previous tmux session - _cleanup_session() is the shared cleanup function for all exit paths Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each study session now runs in its own directory at
~/.config/studyctl/sessions/{session-name}/. The AI agent's
conversation history (.claude/, .kiro/, etc.) is preserved there.
This enables true conversation resume: studyctl study --resume
detects the existing session directory, passes -r to the agent
(e.g. claude -r), and the AI picks up where it left off — not
just tmux reattachment but actual conversation continuity.
Changes:
- tmux.py: create_session accepts cwd parameter
- agent_launcher.py: get_launch_command accepts resume=True,
uses agent's resume command template (claude -r)
- _study.py: creates session dir, detects existing .claude/ for
auto-resume, stores session_dir + agent in state
- _handle_resume: two scenarios — live tmux (reattach) or dead
tmux with preserved dir (rebuild + resume conversation)
- Session dirs persist after --end (conversation history kept)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
agent_launcher.py traverses from src/studyctl/ to the repo root to find agents/shared/personas/. Was off by one parent level, landing in packages/ instead of the repo root. Persona files (study.md, co-study.md) were silently falling back to inline defaults instead of using the richer persona templates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Same class of issue as the sidebar launch — uv run depends on
being in a project directory. Use shutil.which("studyctl") first
(installed entry point), fall back to python -m.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Document the full session lifecycle: persistent session directories, conversation history resume with -r, auto-cleanup on agent exit, sidebar Q to end session. Update roadmap Phase 1 description. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comprehensive document showing how all components connect: - Content pipeline (Obsidian → pandoc → NotebookLM → artefacts) - Study sessions (tmux + agent + sidebar + web dashboard) - Review system (SM-2 spaced repetition via Web PWA) - Session intelligence (export, search, cross-machine sync) - Agent protocol (shared Socratic engine, personas, AuDHD framework) - End-to-end workflow example from materials to mastery - Data stores, prerequisites, and install options Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
TimerWidget now has two modes: - elapsed: counts up with energy-adaptive colour phases (unchanged) - pomodoro: 25/5/25/5/25/5/25/15 cycle with countdown display Sidebar shows: "12:34 FOCUS 2/4" during work blocks, "04:12 BREAK" during short breaks, "08:30 LONG BREAK" during long breaks. Colours: green=focus, cyan=short break, magenta=long break. The tmux status bar oneline also reflects pomodoro state: "Topic | 12:34 F2 | E:7 | W:3 P:1 R:2" Start a pomodoro session with: studyctl study "topic" --timer pomodoro studyctl study "topic" --mode co-study (defaults to pomodoro) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Bring the single source of truth current with all work from the feat+live-session-dashboard branch: studyctl study, tmux, sidebar, pomodoro, auto-cleanup, session directories, resume. Reorganise next steps into v2.2 Polish, Phase 3 Devices, Phase 6 CI/CD. Add key file references for new modules. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues preventing --resume from working: 1. is_session_active() only checked study_session_id, not mode. After cleanup writes mode="ended", the session appeared active. Fix: return False when mode == "ended". 2. _handle_resume didn't clear stale IPC files before calling _handle_start, so _handle_start saw "already active". Fix: call clear_session_files() before rebuilding. Also fixed mode="ended" leaking into the rebuilt session (was passing "ended" as the session mode instead of "study"). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes for --resume: 1. Resume now passes the existing session_name and session_dir to _handle_start, so the rebuilt tmux session lands in the same directory with the preserved .claude/ conversation history. Previously it created a fresh session dir (new study_id). 2. Both _cleanup_session and _handle_end now capture session context (topics covered, wins, struggles, parked questions) as notes in the study_sessions DB record before ending. This gives the agent context about where the conversation was when it resumes. Also fixed is_session_active() to return False when mode=="ended", preventing the "already active" block on resume. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Claude Code stores conversation history in ~/.claude/projects/{path}/,
not .claude/ in the working directory. The resume detection was checking
(session_dir / ".claude").exists() which was always False.
Now checks ~/.claude/projects/{mangled-session-path}/ which is where
Claude actually writes its history. Also moved Path import to cover
both branches.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When ending via sidebar Q, send C-c to the agent pane first so Claude Code exits gracefully and writes its conversation history. Wait 2 seconds for the write, then run cleanup. Without this, killing the tmux session terminates Claude abruptly and the conversation history may not be saved for --resume. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Three fixes for sidebar activity and resume quality: 1. New `studyctl topic` CLI command — agents call this to log topics to the sidebar activity feed. Replaces unreliable raw file writes. Example: studyctl topic "Closures" --status win --note "understood" 2. Personas rewritten to use CLI commands (studyctl topic, studyctl park) instead of raw file writes. Clear instructions on WHEN to log (after every significant exchange). 3. Resume injects previous session notes into the persona file. The agent gets context like "Wins: Decorators. Struggling: Metaclasses. Parked: How does asyncio compare?" and is told to pick up where the student left off. Also: sidebar Q simplified — sends C-c to agent pane and exits. The wrapper command handles cleanup (no competing cleanup calls). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
… stage 3/7) Closes R-05. Before this stage, an unrecognised value for `transport` — including the retired `ttyd` — silently resolved to `pty`, both in the request body (_resolve_transport fell through to a default) and via the STUDYLOOP_TRANSPORT env-var kill switch. A caller asking for a transport that no longer exists got a different one with no error, which is worse than a clean rejection: it hides the fact that the thing asked for is gone. `StartSessionRequest.transport` is now `Literal["pty", "acp"] | None`, so Pydantic itself 422s an unrecognised body value before the handler runs. `_resolve_transport()` raises the new `UnsupportedTransportError` for any non-empty `STUDYLOOP_TRANSPORT` other than "pty"; `start_session()` catches it and returns 422. Both paths are fixed in this one commit, per REMEDIATION-PLAN's Gate 1 amendment #2, with a mandatory test for each: `{"transport": "ttyd"}` -> 422, and `STUDYLOOP_TRANSPORT=ttyd` -> 422 (not silently pty). Deletes `_start_ttyd_session()` and `_ttyd_credentials()` (_start.py) — the dispatcher's fallback branch that reached the legacy tmux+ttyd path is gone along with them. `app.state.lan_username`/`lan_password` (web/app.py) are removed too: `_ttyd_credentials()` was their only reader, and the plan's own instruction is to leave no false authority markers rather than keep unread state whose documented purpose no longer applies. Real LAN Basic-Auth is unaffected — `create_app()` still wires `BasicAuthMiddleware` directly from its username/password parameters, which never went through app.state. Test reconciliation, beyond the manifest's literal line numbers where its granularity didn't reach: TestLanCredentialsOnAppState (test_lan_auth.py, 5 tests) is deleted as one unit — its whole premise (app.state as the ttyd/app auth divergence guard) is gone; test_start_rejects_no_tmux (test_web_session.py) is deleted since no surviving transport touches tmux, so its 503-on-no-multiplexer assertion has no home. Full ledger and rationale for each in evidence/M1/stage-3/04-manifest.md. Pass count 3875 -> 3870, exactly at the strict-manifest floor (3877 - 4 - 3 = 3870). Docs describing the ttyd server transport as still available to maintainers (system-overview.md, architecture/current.md:149, cli-reference.md, troubleshooting.md) are now stale and deliberately left for stage 7 per PLAN-retire-ttyd.md's cut-point rule ("not shippable in doc terms mid-flight ... never cut 0.1.0 mid-flight"). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NetDevAutomate
pushed a commit
that referenced
this pull request
Sep 3, 2026
The encrypted secrets store (secrets.py, commit 5b07afe) was a sealed vault: nothing read from it at generation time and the provider dropdown ignored it. A key added via the web store had zero effect. This wires both ends. - anthropic_compat / openai_compat _read_api_key: call secrets.get_secret(slug) (resolves store -> env -> None) instead of os.environ only. A key stored via the Generate panel is now actually consumed. Error message points users at the panel as well as the env var. Dropped the now-unused os import. - web /content/providers: 'available' flag is now get_secret(slug) OR env var, so a provider with a stored-but-not-exported key shows as enabled, not greyed out. Bedrock's separate boto3-credential path is untouched. TDD: 3 new tests (stored-key consumed at construction; missing-everywhere error names both paths; provider available from store). Red confirmed before fix. 36/36 adapter+providers suites green; 66 secrets/route/content-gen regression tests pass. Remaining for the full feature: the Generate-panel key-entry UI (#2) — backend secrets routes exist + tested, frontend does not call them yet.
NetDevAutomate
pushed a commit
that referenced
this pull request
Sep 3, 2026
Completes the original feature: a user who selects a provider with no stored
key can now enter it in the UI. Previously the backend secrets routes existed
and were tested, but the frontend never called them — there was no way to add
a key through the web app.
- index.html: inline key-entry row, shown only when the selected provider is
keyed (adapter != bedrock) AND not yet available. Password input + 'Test &
save' -> POST /api/content/secrets; on success clears the raw key from
memory, re-fetches /providers so the option flips to enabled, shows a
'verified & stored encrypted' confirmation; on 400/422 shows the provider's
rejection message. needsKey / selectedProvider getters; keyEntry state reset
on provider change.
- style.css: minimal feedback styles (key-error/key-ok/key-hint) using existing
palette vars; password/text inputs styled to match selects.
- test_web_key_entry_e2e.py: 4 real-browser e2e (port 18582) — form shows for
unavailable keyed provider, hidden for available one, save POSTs correct
{provider,key} body + shows success, rejection shows error.
Gate: 4/4 new e2e + 10/10 existing content-gen e2e green. With #1/#3 (4f1425c)
the feature now works end-to-end: select provider -> enter missing key ->
tested + stored encrypted -> consumed at generation.
NetDevAutomate
pushed a commit
that referenced
this pull request
Sep 3, 2026
The panel works live; the tests' locators were wrong. Each provider row renders
ALL THREE auth-kind control divs (api_key / bedrock_bearer / local_keyless),
x-show-hidden but present in the DOM, and every row has its own .key-ok /
.key-error status spans. So:
- unscoped input[type=password] / "Test & save" matched a visible control AND a
hidden one -> strict-mode violation;
- page.wait_for_selector(".provider-row .key-ok") matched all 3 rows and waited
on the first (openai's), which never becomes visible -> timeout.
Fix (selectors only, panel untouched, per handoff):
- scope inputs/buttons with :visible;
- assert "no VISIBLE password input" for Ollama (hidden ones legitimately exist);
- wait on the acted-on row's own .key-ok/.key-error via row.locator(...).wait_for.
All 8 settings-panel e2e tests pass (was 6 failed / 2 passed).
NetDevAutomate
pushed a commit
that referenced
this pull request
Sep 3, 2026
…-side ACP guard (P0) Audit P0 #1 + #2. The user-controlled topic became a session-dir path segment via a naive .replace(' ','-') slug in FOUR places, none of which stripped '/', '\\' or '..' — and the dir is later rmtree'd on failure, so a topic like '../../x' was a real escape+delete vector. - New slug_session_dir() collapses everything outside [a-z0-9] to '-' and falls back to 'session' when empty; session_dir_name() routes through it. - Replace the three inline duplicate slugs (web PTY/ttyd _start.py, CLI session/start.py) with the shared helper — root-cause fix, one segment. - Add server-side ACP capability guard in _start_acp_session: a PTY-only agent (Claude Code, Codex) requesting transport=acp now gets a 400 with cause + repair BEFORE any spawn, instead of an opaque failure. - ACP_CAPABLE_AGENTS is the single source of truth; _options.py's picker flags now reference it instead of a duplicated {kiro,gemini,grok} literal. Tests: parametrized traversal cases + empty-slug fallback + ACP-guard rejection for claude/codex + capable-set lock. Held-out gate: the traversal tests fail on the pre-fix slug (verified by revert), pass on the fix.
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
… stage 3/7) Closes R-05. Before this stage, an unrecognised value for `transport` — including the retired `ttyd` — silently resolved to `pty`, both in the request body (_resolve_transport fell through to a default) and via the STUDYLOOP_TRANSPORT env-var kill switch. A caller asking for a transport that no longer exists got a different one with no error, which is worse than a clean rejection: it hides the fact that the thing asked for is gone. `StartSessionRequest.transport` is now `Literal["pty", "acp"] | None`, so Pydantic itself 422s an unrecognised body value before the handler runs. `_resolve_transport()` raises the new `UnsupportedTransportError` for any non-empty `STUDYLOOP_TRANSPORT` other than "pty"; `start_session()` catches it and returns 422. Both paths are fixed in this one commit, per REMEDIATION-PLAN's Gate 1 amendment #2, with a mandatory test for each: `{"transport": "ttyd"}` -> 422, and `STUDYLOOP_TRANSPORT=ttyd` -> 422 (not silently pty). Deletes `_start_ttyd_session()` and `_ttyd_credentials()` (_start.py) — the dispatcher's fallback branch that reached the legacy tmux+ttyd path is gone along with them. `app.state.lan_username`/`lan_password` (web/app.py) are removed too: `_ttyd_credentials()` was their only reader, and the plan's own instruction is to leave no false authority markers rather than keep unread state whose documented purpose no longer applies. Real LAN Basic-Auth is unaffected — `create_app()` still wires `BasicAuthMiddleware` directly from its username/password parameters, which never went through app.state. Test reconciliation, beyond the manifest's literal line numbers where its granularity didn't reach: TestLanCredentialsOnAppState (test_lan_auth.py, 5 tests) is deleted as one unit — its whole premise (app.state as the ttyd/app auth divergence guard) is gone; test_start_rejects_no_tmux (test_web_session.py) is deleted since no surviving transport touches tmux, so its 503-on-no-multiplexer assertion has no home. Full ledger and rationale for each in evidence/M1/stage-3/04-manifest.md. Pass count 3875 -> 3870, exactly at the strict-manifest floor (3877 - 4 - 3 = 3870). Docs describing the ttyd server transport as still available to maintainers (system-overview.md, architecture/current.md:149, cli-reference.md, troubleshooting.md) are now stale and deliberately left for stage 7 per PLAN-retire-ttyd.md's cut-point rule ("not shippable in doc terms mid-flight ... never cut 0.1.0 mid-flight"). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NetDevAutomate
pushed a commit
that referenced
this pull request
Sep 3, 2026
The panel works live; the tests' locators were wrong. Each provider row renders
ALL THREE auth-kind control divs (api_key / bedrock_bearer / local_keyless),
x-show-hidden but present in the DOM, and every row has its own .key-ok /
.key-error status spans. So:
- unscoped input[type=password] / "Test & save" matched a visible control AND a
hidden one -> strict-mode violation;
- page.wait_for_selector(".provider-row .key-ok") matched all 3 rows and waited
on the first (openai's), which never becomes visible -> timeout.
Fix (selectors only, panel untouched, per handoff):
- scope inputs/buttons with :visible;
- assert "no VISIBLE password input" for Ollama (hidden ones legitimately exist);
- wait on the acted-on row's own .key-ok/.key-error via row.locator(...).wait_for.
All 8 settings-panel e2e tests pass (was 6 failed / 2 passed).
NetDevAutomate
pushed a commit
that referenced
this pull request
Sep 3, 2026
…-side ACP guard (P0) Audit P0 #1 + #2. The user-controlled topic became a session-dir path segment via a naive .replace(' ','-') slug in FOUR places, none of which stripped '/', '\\' or '..' — and the dir is later rmtree'd on failure, so a topic like '../../x' was a real escape+delete vector. - New slug_session_dir() collapses everything outside [a-z0-9] to '-' and falls back to 'session' when empty; session_dir_name() routes through it. - Replace the three inline duplicate slugs (web PTY/ttyd _start.py, CLI session/start.py) with the shared helper — root-cause fix, one segment. - Add server-side ACP capability guard in _start_acp_session: a PTY-only agent (Claude Code, Codex) requesting transport=acp now gets a 400 with cause + repair BEFORE any spawn, instead of an opaque failure. - ACP_CAPABLE_AGENTS is the single source of truth; _options.py's picker flags now reference it instead of a duplicated {kiro,gemini,grok} literal. Tests: parametrized traversal cases + empty-slug fallback + ACP-guard rejection for claude/codex + capable-set lock. Held-out gate: the traversal tests fail on the pre-fix slug (verified by revert), pass on the fix.
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
… stage 3/7) Closes R-05. Before this stage, an unrecognised value for `transport` — including the retired `ttyd` — silently resolved to `pty`, both in the request body (_resolve_transport fell through to a default) and via the STUDYLOOP_TRANSPORT env-var kill switch. A caller asking for a transport that no longer exists got a different one with no error, which is worse than a clean rejection: it hides the fact that the thing asked for is gone. `StartSessionRequest.transport` is now `Literal["pty", "acp"] | None`, so Pydantic itself 422s an unrecognised body value before the handler runs. `_resolve_transport()` raises the new `UnsupportedTransportError` for any non-empty `STUDYLOOP_TRANSPORT` other than "pty"; `start_session()` catches it and returns 422. Both paths are fixed in this one commit, per REMEDIATION-PLAN's Gate 1 amendment #2, with a mandatory test for each: `{"transport": "ttyd"}` -> 422, and `STUDYLOOP_TRANSPORT=ttyd` -> 422 (not silently pty). Deletes `_start_ttyd_session()` and `_ttyd_credentials()` (_start.py) — the dispatcher's fallback branch that reached the legacy tmux+ttyd path is gone along with them. `app.state.lan_username`/`lan_password` (web/app.py) are removed too: `_ttyd_credentials()` was their only reader, and the plan's own instruction is to leave no false authority markers rather than keep unread state whose documented purpose no longer applies. Real LAN Basic-Auth is unaffected — `create_app()` still wires `BasicAuthMiddleware` directly from its username/password parameters, which never went through app.state. Test reconciliation, beyond the manifest's literal line numbers where its granularity didn't reach: TestLanCredentialsOnAppState (test_lan_auth.py, 5 tests) is deleted as one unit — its whole premise (app.state as the ttyd/app auth divergence guard) is gone; test_start_rejects_no_tmux (test_web_session.py) is deleted since no surviving transport touches tmux, so its 503-on-no-multiplexer assertion has no home. Full ledger and rationale for each in evidence/M1/stage-3/04-manifest.md. Pass count 3875 -> 3870, exactly at the strict-manifest floor (3877 - 4 - 3 = 3870). Docs describing the ttyd server transport as still available to maintainers (system-overview.md, architecture/current.md:149, cli-reference.md, troubleshooting.md) are now stale and deliberately left for stage 7 per PLAN-retire-ttyd.md's cut-point rule ("not shippable in doc terms mid-flight ... never cut 0.1.0 mid-flight"). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NetDevAutomate
pushed a commit
that referenced
this pull request
Sep 3, 2026
The encrypted secrets store (secrets.py, commit 7147ca6) was a sealed vault: nothing read from it at generation time and the provider dropdown ignored it. A key added via the web store had zero effect. This wires both ends. - anthropic_compat / openai_compat _read_api_key: call secrets.get_secret(slug) (resolves store -> env -> None) instead of os.environ only. A key stored via the Generate panel is now actually consumed. Error message points users at the panel as well as the env var. Dropped the now-unused os import. - web /content/providers: 'available' flag is now get_secret(slug) OR env var, so a provider with a stored-but-not-exported key shows as enabled, not greyed out. Bedrock's separate boto3-credential path is untouched. TDD: 3 new tests (stored-key consumed at construction; missing-everywhere error names both paths; provider available from store). Red confirmed before fix. 36/36 adapter+providers suites green; 66 secrets/route/content-gen regression tests pass. Remaining for the full feature: the Generate-panel key-entry UI (#2) — backend secrets routes exist + tested, frontend does not call them yet.
NetDevAutomate
pushed a commit
that referenced
this pull request
Sep 3, 2026
Completes the original feature: a user who selects a provider with no stored
key can now enter it in the UI. Previously the backend secrets routes existed
and were tested, but the frontend never called them — there was no way to add
a key through the web app.
- index.html: inline key-entry row, shown only when the selected provider is
keyed (adapter != bedrock) AND not yet available. Password input + 'Test &
save' -> POST /api/content/secrets; on success clears the raw key from
memory, re-fetches /providers so the option flips to enabled, shows a
'verified & stored encrypted' confirmation; on 400/422 shows the provider's
rejection message. needsKey / selectedProvider getters; keyEntry state reset
on provider change.
- style.css: minimal feedback styles (key-error/key-ok/key-hint) using existing
palette vars; password/text inputs styled to match selects.
- test_web_key_entry_e2e.py: 4 real-browser e2e (port 18582) — form shows for
unavailable keyed provider, hidden for available one, save POSTs correct
{provider,key} body + shows success, rejection shows error.
Gate: 4/4 new e2e + 10/10 existing content-gen e2e green. With #1/#3 (4f1425c)
the feature now works end-to-end: select provider -> enter missing key ->
tested + stored encrypted -> consumed at generation.
NetDevAutomate
pushed a commit
that referenced
this pull request
Sep 3, 2026
The panel works live; the tests' locators were wrong. Each provider row renders
ALL THREE auth-kind control divs (api_key / bedrock_bearer / local_keyless),
x-show-hidden but present in the DOM, and every row has its own .key-ok /
.key-error status spans. So:
- unscoped input[type=password] / "Test & save" matched a visible control AND a
hidden one -> strict-mode violation;
- page.wait_for_selector(".provider-row .key-ok") matched all 3 rows and waited
on the first (openai's), which never becomes visible -> timeout.
Fix (selectors only, panel untouched, per handoff):
- scope inputs/buttons with :visible;
- assert "no VISIBLE password input" for Ollama (hidden ones legitimately exist);
- wait on the acted-on row's own .key-ok/.key-error via row.locator(...).wait_for.
All 8 settings-panel e2e tests pass (was 6 failed / 2 passed).
NetDevAutomate
pushed a commit
that referenced
this pull request
Sep 3, 2026
…-side ACP guard (P0) Audit P0 #1 + #2. The user-controlled topic became a session-dir path segment via a naive .replace(' ','-') slug in FOUR places, none of which stripped '/', '\\' or '..' — and the dir is later rmtree'd on failure, so a topic like '../../x' was a real escape+delete vector. - New slug_session_dir() collapses everything outside [a-z0-9] to '-' and falls back to 'session' when empty; session_dir_name() routes through it. - Replace the three inline duplicate slugs (web PTY/ttyd _start.py, CLI session/start.py) with the shared helper — root-cause fix, one segment. - Add server-side ACP capability guard in _start_acp_session: a PTY-only agent (Claude Code, Codex) requesting transport=acp now gets a 400 with cause + repair BEFORE any spawn, instead of an opaque failure. - ACP_CAPABLE_AGENTS is the single source of truth; _options.py's picker flags now reference it instead of a duplicated {kiro,gemini,grok} literal. Tests: parametrized traversal cases + empty-slug fallback + ACP-guard rejection for claude/codex + capable-set lock. Held-out gate: the traversal tests fail on the pre-fix slug (verified by revert), pass on the fix.
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
… stage 3/7) Closes R-05. Before this stage, an unrecognised value for `transport` — including the retired `ttyd` — silently resolved to `pty`, both in the request body (_resolve_transport fell through to a default) and via the STUDYLOOP_TRANSPORT env-var kill switch. A caller asking for a transport that no longer exists got a different one with no error, which is worse than a clean rejection: it hides the fact that the thing asked for is gone. `StartSessionRequest.transport` is now `Literal["pty", "acp"] | None`, so Pydantic itself 422s an unrecognised body value before the handler runs. `_resolve_transport()` raises the new `UnsupportedTransportError` for any non-empty `STUDYLOOP_TRANSPORT` other than "pty"; `start_session()` catches it and returns 422. Both paths are fixed in this one commit, per REMEDIATION-PLAN's Gate 1 amendment #2, with a mandatory test for each: `{"transport": "ttyd"}` -> 422, and `STUDYLOOP_TRANSPORT=ttyd` -> 422 (not silently pty). Deletes `_start_ttyd_session()` and `_ttyd_credentials()` (_start.py) — the dispatcher's fallback branch that reached the legacy tmux+ttyd path is gone along with them. `app.state.lan_username`/`lan_password` (web/app.py) are removed too: `_ttyd_credentials()` was their only reader, and the plan's own instruction is to leave no false authority markers rather than keep unread state whose documented purpose no longer applies. Real LAN Basic-Auth is unaffected — `create_app()` still wires `BasicAuthMiddleware` directly from its username/password parameters, which never went through app.state. Test reconciliation, beyond the manifest's literal line numbers where its granularity didn't reach: TestLanCredentialsOnAppState (test_lan_auth.py, 5 tests) is deleted as one unit — its whole premise (app.state as the ttyd/app auth divergence guard) is gone; test_start_rejects_no_tmux (test_web_session.py) is deleted since no surviving transport touches tmux, so its 503-on-no-multiplexer assertion has no home. Full ledger and rationale for each in evidence/M1/stage-3/04-manifest.md. Pass count 3875 -> 3870, exactly at the strict-manifest floor (3877 - 4 - 3 = 3870). Docs describing the ttyd server transport as still available to maintainers (system-overview.md, architecture/current.md:149, cli-reference.md, troubleshooting.md) are now stale and deliberately left for stage 7 per PLAN-retire-ttyd.md's cut-point rule ("not shippable in doc terms mid-flight ... never cut 0.1.0 mid-flight"). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
The encrypted secrets store (secrets.py, commit fd294f4) was a sealed vault: nothing read from it at generation time and the provider dropdown ignored it. A key added via the web store had zero effect. This wires both ends. - anthropic_compat / openai_compat _read_api_key: call secrets.get_secret(slug) (resolves store -> env -> None) instead of os.environ only. A key stored via the Generate panel is now actually consumed. Error message points users at the panel as well as the env var. Dropped the now-unused os import. - web /content/providers: 'available' flag is now get_secret(slug) OR env var, so a provider with a stored-but-not-exported key shows as enabled, not greyed out. Bedrock's separate boto3-credential path is untouched. TDD: 3 new tests (stored-key consumed at construction; missing-everywhere error names both paths; provider available from store). Red confirmed before fix. 36/36 adapter+providers suites green; 66 secrets/route/content-gen regression tests pass. Remaining for the full feature: the Generate-panel key-entry UI (#2) — backend secrets routes exist + tested, frontend does not call them yet.
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
Completes the original feature: a user who selects a provider with no stored
key can now enter it in the UI. Previously the backend secrets routes existed
and were tested, but the frontend never called them — there was no way to add
a key through the web app.
- index.html: inline key-entry row, shown only when the selected provider is
keyed (adapter != bedrock) AND not yet available. Password input + 'Test &
save' -> POST /api/content/secrets; on success clears the raw key from
memory, re-fetches /providers so the option flips to enabled, shows a
'verified & stored encrypted' confirmation; on 400/422 shows the provider's
rejection message. needsKey / selectedProvider getters; keyEntry state reset
on provider change.
- style.css: minimal feedback styles (key-error/key-ok/key-hint) using existing
palette vars; password/text inputs styled to match selects.
- test_web_key_entry_e2e.py: 4 real-browser e2e (port 18582) — form shows for
unavailable keyed provider, hidden for available one, save POSTs correct
{provider,key} body + shows success, rejection shows error.
Gate: 4/4 new e2e + 10/10 existing content-gen e2e green. With #1/#3 (4f1425c)
the feature now works end-to-end: select provider -> enter missing key ->
tested + stored encrypted -> consumed at generation.
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
The panel works live; the tests' locators were wrong. Each provider row renders
ALL THREE auth-kind control divs (api_key / bedrock_bearer / local_keyless),
x-show-hidden but present in the DOM, and every row has its own .key-ok /
.key-error status spans. So:
- unscoped input[type=password] / "Test & save" matched a visible control AND a
hidden one -> strict-mode violation;
- page.wait_for_selector(".provider-row .key-ok") matched all 3 rows and waited
on the first (openai's), which never becomes visible -> timeout.
Fix (selectors only, panel untouched, per handoff):
- scope inputs/buttons with :visible;
- assert "no VISIBLE password input" for Ollama (hidden ones legitimately exist);
- wait on the acted-on row's own .key-ok/.key-error via row.locator(...).wait_for.
All 8 settings-panel e2e tests pass (was 6 failed / 2 passed).
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
…-side ACP guard (P0) Audit P0 #1 + #2. The user-controlled topic became a session-dir path segment via a naive .replace(' ','-') slug in FOUR places, none of which stripped '/', '\\' or '..' — and the dir is later rmtree'd on failure, so a topic like '../../x' was a real escape+delete vector. - New slug_session_dir() collapses everything outside [a-z0-9] to '-' and falls back to 'session' when empty; session_dir_name() routes through it. - Replace the three inline duplicate slugs (web PTY/ttyd _start.py, CLI session/start.py) with the shared helper — root-cause fix, one segment. - Add server-side ACP capability guard in _start_acp_session: a PTY-only agent (Claude Code, Codex) requesting transport=acp now gets a 400 with cause + repair BEFORE any spawn, instead of an opaque failure. - ACP_CAPABLE_AGENTS is the single source of truth; _options.py's picker flags now reference it instead of a duplicated {kiro,gemini,grok} literal. Tests: parametrized traversal cases + empty-slug fallback + ACP-guard rejection for claude/codex + capable-set lock. Held-out gate: the traversal tests fail on the pre-fix slug (verified by revert), pass on the fix.
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
… stage 3/7) Closes R-05. Before this stage, an unrecognised value for `transport` — including the retired `ttyd` — silently resolved to `pty`, both in the request body (_resolve_transport fell through to a default) and via the STUDYLOOP_TRANSPORT env-var kill switch. A caller asking for a transport that no longer exists got a different one with no error, which is worse than a clean rejection: it hides the fact that the thing asked for is gone. `StartSessionRequest.transport` is now `Literal["pty", "acp"] | None`, so Pydantic itself 422s an unrecognised body value before the handler runs. `_resolve_transport()` raises the new `UnsupportedTransportError` for any non-empty `STUDYLOOP_TRANSPORT` other than "pty"; `start_session()` catches it and returns 422. Both paths are fixed in this one commit, per REMEDIATION-PLAN's Gate 1 amendment #2, with a mandatory test for each: `{"transport": "ttyd"}` -> 422, and `STUDYLOOP_TRANSPORT=ttyd` -> 422 (not silently pty). Deletes `_start_ttyd_session()` and `_ttyd_credentials()` (_start.py) — the dispatcher's fallback branch that reached the legacy tmux+ttyd path is gone along with them. `app.state.lan_username`/`lan_password` (web/app.py) are removed too: `_ttyd_credentials()` was their only reader, and the plan's own instruction is to leave no false authority markers rather than keep unread state whose documented purpose no longer applies. Real LAN Basic-Auth is unaffected — `create_app()` still wires `BasicAuthMiddleware` directly from its username/password parameters, which never went through app.state. Test reconciliation, beyond the manifest's literal line numbers where its granularity didn't reach: TestLanCredentialsOnAppState (test_lan_auth.py, 5 tests) is deleted as one unit — its whole premise (app.state as the ttyd/app auth divergence guard) is gone; test_start_rejects_no_tmux (test_web_session.py) is deleted since no surviving transport touches tmux, so its 503-on-no-multiplexer assertion has no home. Full ledger and rationale for each in evidence/M1/stage-3/04-manifest.md. Pass count 3875 -> 3870, exactly at the strict-manifest floor (3877 - 4 - 3 = 3870). Docs describing the ttyd server transport as still available to maintainers (system-overview.md, architecture/current.md:149, cli-reference.md, troubleshooting.md) are now stale and deliberately left for stage 7 per PLAN-retire-ttyd.md's cut-point rule ("not shippable in doc terms mid-flight ... never cut 0.1.0 mid-flight"). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
The encrypted secrets store (secrets.py, commit 4e7207a) was a sealed vault: nothing read from it at generation time and the provider dropdown ignored it. A key added via the web store had zero effect. This wires both ends. - anthropic_compat / openai_compat _read_api_key: call secrets.get_secret(slug) (resolves store -> env -> None) instead of os.environ only. A key stored via the Generate panel is now actually consumed. Error message points users at the panel as well as the env var. Dropped the now-unused os import. - web /content/providers: 'available' flag is now get_secret(slug) OR env var, so a provider with a stored-but-not-exported key shows as enabled, not greyed out. Bedrock's separate boto3-credential path is untouched. TDD: 3 new tests (stored-key consumed at construction; missing-everywhere error names both paths; provider available from store). Red confirmed before fix. 36/36 adapter+providers suites green; 66 secrets/route/content-gen regression tests pass. Remaining for the full feature: the Generate-panel key-entry UI (#2) — backend secrets routes exist + tested, frontend does not call them yet.
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
Completes the original feature: a user who selects a provider with no stored
key can now enter it in the UI. Previously the backend secrets routes existed
and were tested, but the frontend never called them — there was no way to add
a key through the web app.
- index.html: inline key-entry row, shown only when the selected provider is
keyed (adapter != bedrock) AND not yet available. Password input + 'Test &
save' -> POST /api/content/secrets; on success clears the raw key from
memory, re-fetches /providers so the option flips to enabled, shows a
'verified & stored encrypted' confirmation; on 400/422 shows the provider's
rejection message. needsKey / selectedProvider getters; keyEntry state reset
on provider change.
- style.css: minimal feedback styles (key-error/key-ok/key-hint) using existing
palette vars; password/text inputs styled to match selects.
- test_web_key_entry_e2e.py: 4 real-browser e2e (port 18582) — form shows for
unavailable keyed provider, hidden for available one, save POSTs correct
{provider,key} body + shows success, rejection shows error.
Gate: 4/4 new e2e + 10/10 existing content-gen e2e green. With #1/#3 (4f1425c)
the feature now works end-to-end: select provider -> enter missing key ->
tested + stored encrypted -> consumed at generation.
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
The panel works live; the tests' locators were wrong. Each provider row renders
ALL THREE auth-kind control divs (api_key / bedrock_bearer / local_keyless),
x-show-hidden but present in the DOM, and every row has its own .key-ok /
.key-error status spans. So:
- unscoped input[type=password] / "Test & save" matched a visible control AND a
hidden one -> strict-mode violation;
- page.wait_for_selector(".provider-row .key-ok") matched all 3 rows and waited
on the first (openai's), which never becomes visible -> timeout.
Fix (selectors only, panel untouched, per handoff):
- scope inputs/buttons with :visible;
- assert "no VISIBLE password input" for Ollama (hidden ones legitimately exist);
- wait on the acted-on row's own .key-ok/.key-error via row.locator(...).wait_for.
All 8 settings-panel e2e tests pass (was 6 failed / 2 passed).
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
…-side ACP guard (P0) Audit P0 #1 + #2. The user-controlled topic became a session-dir path segment via a naive .replace(' ','-') slug in FOUR places, none of which stripped '/', '\\' or '..' — and the dir is later rmtree'd on failure, so a topic like '../../x' was a real escape+delete vector. - New slug_session_dir() collapses everything outside [a-z0-9] to '-' and falls back to 'session' when empty; session_dir_name() routes through it. - Replace the three inline duplicate slugs (web PTY/ttyd _start.py, CLI session/start.py) with the shared helper — root-cause fix, one segment. - Add server-side ACP capability guard in _start_acp_session: a PTY-only agent (Claude Code, Codex) requesting transport=acp now gets a 400 with cause + repair BEFORE any spawn, instead of an opaque failure. - ACP_CAPABLE_AGENTS is the single source of truth; _options.py's picker flags now reference it instead of a duplicated {kiro,gemini,grok} literal. Tests: parametrized traversal cases + empty-slug fallback + ACP-guard rejection for claude/codex + capable-set lock. Held-out gate: the traversal tests fail on the pre-fix slug (verified by revert), pass on the fix.
NetDevAutomate
added a commit
that referenced
this pull request
Sep 3, 2026
… stage 3/7) Closes R-05. Before this stage, an unrecognised value for `transport` — including the retired `ttyd` — silently resolved to `pty`, both in the request body (_resolve_transport fell through to a default) and via the STUDYLOOP_TRANSPORT env-var kill switch. A caller asking for a transport that no longer exists got a different one with no error, which is worse than a clean rejection: it hides the fact that the thing asked for is gone. `StartSessionRequest.transport` is now `Literal["pty", "acp"] | None`, so Pydantic itself 422s an unrecognised body value before the handler runs. `_resolve_transport()` raises the new `UnsupportedTransportError` for any non-empty `STUDYLOOP_TRANSPORT` other than "pty"; `start_session()` catches it and returns 422. Both paths are fixed in this one commit, per REMEDIATION-PLAN's Gate 1 amendment #2, with a mandatory test for each: `{"transport": "ttyd"}` -> 422, and `STUDYLOOP_TRANSPORT=ttyd` -> 422 (not silently pty). Deletes `_start_ttyd_session()` and `_ttyd_credentials()` (_start.py) — the dispatcher's fallback branch that reached the legacy tmux+ttyd path is gone along with them. `app.state.lan_username`/`lan_password` (web/app.py) are removed too: `_ttyd_credentials()` was their only reader, and the plan's own instruction is to leave no false authority markers rather than keep unread state whose documented purpose no longer applies. Real LAN Basic-Auth is unaffected — `create_app()` still wires `BasicAuthMiddleware` directly from its username/password parameters, which never went through app.state. Test reconciliation, beyond the manifest's literal line numbers where its granularity didn't reach: TestLanCredentialsOnAppState (test_lan_auth.py, 5 tests) is deleted as one unit — its whole premise (app.state as the ttyd/app auth divergence guard) is gone; test_start_rejects_no_tmux (test_web_session.py) is deleted since no surviving transport touches tmux, so its 503-on-no-multiplexer assertion has no home. Full ledger and rationale for each in evidence/M1/stage-3/04-manifest.md. Pass count 3875 -> 3870, exactly at the strict-manifest floor (3877 - 4 - 3 = 3870). Docs describing the ttyd server transport as still available to maintainers (system-overview.md, architecture/current.md:149, cli-reference.md, troubleshooting.md) are now stale and deliberately left for stage 7 per PLAN-retire-ttyd.md's cut-point rule ("not shippable in doc terms mid-flight ... never cut 0.1.0 mid-flight"). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
NetDevAutomate
added a commit
that referenced
this pull request
Sep 5, 2026
feat: merge study command + E2E experience tests + bridge CLI
NetDevAutomate
added a commit
that referenced
this pull request
Sep 5, 2026
The encrypted secrets store (secrets.py, commit f788055) was a sealed vault: nothing read from it at generation time and the provider dropdown ignored it. A key added via the web store had zero effect. This wires both ends. - anthropic_compat / openai_compat _read_api_key: call secrets.get_secret(slug) (resolves store -> env -> None) instead of os.environ only. A key stored via the Generate panel is now actually consumed. Error message points users at the panel as well as the env var. Dropped the now-unused os import. - web /content/providers: 'available' flag is now get_secret(slug) OR env var, so a provider with a stored-but-not-exported key shows as enabled, not greyed out. Bedrock's separate boto3-credential path is untouched. TDD: 3 new tests (stored-key consumed at construction; missing-everywhere error names both paths; provider available from store). Red confirmed before fix. 36/36 adapter+providers suites green; 66 secrets/route/content-gen regression tests pass. Remaining for the full feature: the Generate-panel key-entry UI (#2) — backend secrets routes exist + tested, frontend does not call them yet.
NetDevAutomate
added a commit
that referenced
this pull request
Sep 5, 2026
Completes the original feature: a user who selects a provider with no stored
key can now enter it in the UI. Previously the backend secrets routes existed
and were tested, but the frontend never called them — there was no way to add
a key through the web app.
- index.html: inline key-entry row, shown only when the selected provider is
keyed (adapter != bedrock) AND not yet available. Password input + 'Test &
save' -> POST /api/content/secrets; on success clears the raw key from
memory, re-fetches /providers so the option flips to enabled, shows a
'verified & stored encrypted' confirmation; on 400/422 shows the provider's
rejection message. needsKey / selectedProvider getters; keyEntry state reset
on provider change.
- style.css: minimal feedback styles (key-error/key-ok/key-hint) using existing
palette vars; password/text inputs styled to match selects.
- test_web_key_entry_e2e.py: 4 real-browser e2e (port 18582) — form shows for
unavailable keyed provider, hidden for available one, save POSTs correct
{provider,key} body + shows success, rejection shows error.
Gate: 4/4 new e2e + 10/10 existing content-gen e2e green. With #1/#3 (4f1425c)
the feature now works end-to-end: select provider -> enter missing key ->
tested + stored encrypted -> consumed at generation.
NetDevAutomate
added a commit
that referenced
this pull request
Sep 5, 2026
The panel works live; the tests' locators were wrong. Each provider row renders
ALL THREE auth-kind control divs (api_key / bedrock_bearer / local_keyless),
x-show-hidden but present in the DOM, and every row has its own .key-ok /
.key-error status spans. So:
- unscoped input[type=password] / "Test & save" matched a visible control AND a
hidden one -> strict-mode violation;
- page.wait_for_selector(".provider-row .key-ok") matched all 3 rows and waited
on the first (openai's), which never becomes visible -> timeout.
Fix (selectors only, panel untouched, per handoff):
- scope inputs/buttons with :visible;
- assert "no VISIBLE password input" for Ollama (hidden ones legitimately exist);
- wait on the acted-on row's own .key-ok/.key-error via row.locator(...).wait_for.
All 8 settings-panel e2e tests pass (was 6 failed / 2 passed).
NetDevAutomate
added a commit
that referenced
this pull request
Sep 5, 2026
…-side ACP guard (P0) Audit P0 #1 + #2. The user-controlled topic became a session-dir path segment via a naive .replace(' ','-') slug in FOUR places, none of which stripped '/', '\\' or '..' — and the dir is later rmtree'd on failure, so a topic like '../../x' was a real escape+delete vector. - New slug_session_dir() collapses everything outside [a-z0-9] to '-' and falls back to 'session' when empty; session_dir_name() routes through it. - Replace the three inline duplicate slugs (web PTY/ttyd _start.py, CLI session/start.py) with the shared helper — root-cause fix, one segment. - Add server-side ACP capability guard in _start_acp_session: a PTY-only agent (Claude Code, Codex) requesting transport=acp now gets a 400 with cause + repair BEFORE any spawn, instead of an opaque failure. - ACP_CAPABLE_AGENTS is the single source of truth; _options.py's picker flags now reference it instead of a duplicated {kiro,gemini,grok} literal. Tests: parametrized traversal cases + empty-slug fallback + ACP-guard rejection for claude/codex + capable-set lock. Held-out gate: the traversal tests fail on the pre-fix slug (verified by revert), pass on the fix.
NetDevAutomate
added a commit
that referenced
this pull request
Sep 5, 2026
… stage 3/7) Closes R-05. Before this stage, an unrecognised value for `transport` — including the retired `ttyd` — silently resolved to `pty`, both in the request body (_resolve_transport fell through to a default) and via the STUDYLOOP_TRANSPORT env-var kill switch. A caller asking for a transport that no longer exists got a different one with no error, which is worse than a clean rejection: it hides the fact that the thing asked for is gone. `StartSessionRequest.transport` is now `Literal["pty", "acp"] | None`, so Pydantic itself 422s an unrecognised body value before the handler runs. `_resolve_transport()` raises the new `UnsupportedTransportError` for any non-empty `STUDYLOOP_TRANSPORT` other than "pty"; `start_session()` catches it and returns 422. Both paths are fixed in this one commit, per REMEDIATION-PLAN's Gate 1 amendment #2, with a mandatory test for each: `{"transport": "ttyd"}` -> 422, and `STUDYLOOP_TRANSPORT=ttyd` -> 422 (not silently pty). Deletes `_start_ttyd_session()` and `_ttyd_credentials()` (_start.py) — the dispatcher's fallback branch that reached the legacy tmux+ttyd path is gone along with them. `app.state.lan_username`/`lan_password` (web/app.py) are removed too: `_ttyd_credentials()` was their only reader, and the plan's own instruction is to leave no false authority markers rather than keep unread state whose documented purpose no longer applies. Real LAN Basic-Auth is unaffected — `create_app()` still wires `BasicAuthMiddleware` directly from its username/password parameters, which never went through app.state. Test reconciliation, beyond the manifest's literal line numbers where its granularity didn't reach: TestLanCredentialsOnAppState (test_lan_auth.py, 5 tests) is deleted as one unit — its whole premise (app.state as the ttyd/app auth divergence guard) is gone; test_start_rejects_no_tmux (test_web_session.py) is deleted since no surviving transport touches tmux, so its 503-on-no-multiplexer assertion has no home. Full ledger and rationale for each in evidence/M1/stage-3/04-manifest.md. Pass count 3875 -> 3870, exactly at the strict-manifest floor (3877 - 4 - 3 = 3870). Docs describing the ttyd server transport as still available to maintainers (system-overview.md, architecture/current.md:149, cli-reference.md, troubleshooting.md) are now stale and deliberately left for stage 7 per PLAN-retire-ttyd.md's cut-point rule ("not shippable in doc terms mid-flight ... never cut 0.1.0 mid-flight"). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
feat+live-session-dashboardworktree — 37 commits addingstudyctl study(tmux orchestrator + Textual sidebar + session state + agent launcher + parking lot)bridgeCLI command — dropped during compaction but DB functions preserved; CLI entry point restoredTest plan
studyctl study --helpworksstudyctl bridge --helpshows add/liststudyctl session --helpshows start/end/statusstudyctl --helpPost-Deploy Monitoring & Validation
No additional operational monitoring required: CLI tool distributed via PyPI — users upgrade at their own pace.
🤖 Generated with Claude Code