Skip to content

Latest commit

 

History

History
1031 lines (969 loc) · 64.2 KB

File metadata and controls

1031 lines (969 loc) · 64.2 KB

Changelog

All notable public changes to StudyLoop are recorded here.

StudyLoop is preparing its first pre-release. The public API and installation experience may change before 1.0.0.

[Unreleased]

Added

  • The now engine's recommendation can carry one passive first move (issue #30). On a low-energy day, the sit-with proposal names one tiny action on the deferred milestone — Open “Advanced Sql 4H” from Complete Sql Databases Bootcamp — the match is the phrase “window function” — and read for ten minutes, nothing more — naming a lesson only when the Course Explorer index matches the milestone's own concepts, and stating the course and the matched concept so a lexical match can be judged from the sentence; when nothing matches it names the milestone and says why (no indexed lesson mentions “window frame” yet). A plan-related active primary carries the same move as a warm-up into itself (… then start the repair / the review / the milestone), never on recall. CLI now prints First move: beneath the door; the Today card shows the line with an Open the lesson control that opens the lesson in the Course Explorer beside the current view; Body Double and Study Session carry the move and the control from the picker into the live session, and the Today card's Start → now hands the Study picker the action's topic, energy and move (it used to hand it nothing). The move belongs to the material it arrived beside: editing or re-picking the topic, a hand-off during a live session, or rejoining another session never shows it under different material. Proposal only — the companion says nothing about it. The no-plan payload is byte-identical.
  • A standalone studyloop-study-notes Agent Skill with per-lesson Markdown and section-overview templates, source/enrichment attribution, and explicit Obsidian-only, xTiles-only, and linked dual-destination workflows. Installed separately through the skills CLI, not by studyloop install agents; its guide is published on the docs site as Lesson Study Notes (docs/study-notes-skill.md).
  • The mentor writes to the learning tier (issue #38, item 1 of the learning-tier plan). A record_teachback MCP tool records a teach-back score through the same validator the studyloop teachback CLI uses (the CLI now delegates to it; its messages are unchanged), so a mentor session's teach-back reaches tomorrow's recommendation instead of stopping at the conversation. One agents/shared/recording-protocol.md carries a machine-readable trigger table naming exactly the four append-only writers (log_topic, log_struggle, record_teachback, record_plan_learning); every harness definition and the built live persona name the same four, and parity tests pin the names, the protocol hash and the absence of any new pre-approval — recording stays prompt-per-call on every harness, by owner decision. Scheduling mutators never fire from the protocol. A child-process isolation guard proves the four writers land nowhere outside the sandbox database. Owner decisions 1–5 are recorded in the plan (#34).

Changed

  • The openspec/ tree is no longer listed in .gitignore. Seventy-eight tracked, load-bearing files lived under an ignored path, so every new spec or archive file was invisible to git status and skipped by git add -A. The herdr-ghostty-multiplexer-transport change is archived as deferred (the owner's 2026-09-05 decision, unchanged: tmux stays the production default, herdr an experimental opt-in) with its five open tasks closed as not built and its spec deltas deliberately not merged — they modified requirements no main spec contains and describe a wterm selector and a ttyd fallback the tree has since retired. The July e2e/MCP archive's four open tasks are reconciled against the tree (all four were built the same day their "not done" notes were written), so openspec validate --archived --all is green for the first time since the release guard was added.

Fixed

  • The now engine counts every "last seen N day(s) ago" from the one clock it reads per plan. The due-progress collector took its day count from a second clock inside history/progress.py, so under a frozen test clock the count — and the score built from it — drifted with the real date (the medium-energy screen in receipt now-rubric-2026-09-16 printed 8 days for a struggle planted 3 days back). Production always read one wall clock, so no learner saw the split; spaced_repetition_due() gains a keyword-only now for callers that already hold an instant, defaulting to the wall clock for everyone else.
  • A browser tab rejoining a session whose transport had already drained no longer leaves Task exception was never retrieved … StopAsyncIteration in the server log. The WebSocket pump pulls each transport event as its own future; when a newer socket took the slot, or the client closed while the drained future was already complete, the pump exited without reading that future's StopAsyncIteration. The pump now retrieves the result of a done pull future on every exit path.

[0.5.0] - 2026-09-21

Added

  • An acceptance-test tier modelled on the Terraform provider's make testacc: every test under packages/studyloop/tests/acceptance/ skips with a named reason unless STUDYLOOP_ACC=1; just testacc selects harnesses (STUDYLOOP_ACC_HARNESS), actors and -k expressions; each test runs in a guarded scratch environment (fresh HOME and state dir, sanitized child env, dedicated tmux socket, symlink/sentinel sweeper guards, an escape-canary test) that can never touch ~/.config/studyloop; a versioned turn-script format drives a deterministic scripted learner; the first live lane runs Kiro over web ACP. The UAT tier is reserved behind an additional STUDYLOOP_UAT=1. See docs/acceptance-testing.md.
  • The acceptance tier covers every harness over the CLI/tmux path: a parametrised matrix drives the real installed binary (named skip when absent or unauthenticated), a budget-guarded tmux PaneDriver records every turn, and each run writes a per-run evidence bundle (pane transcript, DB delta, harness version/platform/auth mode). Coverage is reported as a harness × surface × transport matrix that separates "test exists (gated)" from recorded clean live runs.
  • The learner side of an acceptance conversation is pluggable (STUDYLOOP_ACC_ACTOR): scripted (deterministic, CI-safe, default), gateway (LiteLLM), direct (OpenAI/Anthropic SDK for contributors without the gateway), and harness (a second harness plays the learner on its own tmux socket) — one async, cancellable LearnerActor protocol with a hard turn/token budget and honest usage reporting; missing credentials are a named skip, never a failure.
  • The UAT sign-off tier's recording infrastructure: a durable evidence-bundle writer (private 0700 run dirs, atomic writes, sha256 file inventory, manifest with harness/actor/rubric provenance, resolved from the real environment before scratch substitution), a redacted-summary generator with versioned, hash-pinned redaction rules, a versioned rubric loader with explicit failure conditions, and a strict runner where a sign-off can never pass through skips or an empty selection. The live LLM/browser journeys and the council-graded sign-off itself are follow-on work, tracked in docs/acceptance-testing.md.
  • session-maint fetch-query-encoder fetches and sha256-verifies the pinned ONNX query-encoder artefact (explicit action only — searches and hooks never download), and studyloop doctor gains a query_encoder_artefact check whose --fix runs the fetch. This makes the degradation message's "fetch it once" promise true.
  • A query-encoder construction seam for the semantic layer (semantic_search.query_encoder: torch|onnx): the query side of a hybrid search can now use a pinned fp32 ONNX bge-small-en-v1.5 (artefact revision + sha256 recorded in the model registry; fetched only by explicit user action, never mid-search), cached by (model, backend, revision) with single-flight construction and load-phase events (default: see Changed).
  • An honest load indicator for encoder loads: timer-driven stderr phase messages past 300 ms ("last load: X s" from per-machine cold/warm records — never a percentage bar), and a web encoder-warm status chip fed by GET /api/retrieval/health.
  • Study Plans reach the surfaces you decide on. One PlanApplication seam (studyloop.planning.application) now carries every plan use case — browse, inspect, prepare planning, active guidance, apply a change, assess — and the CLI, the Web UI and the MCP server all go through it. Two bugs that seam exists because of are closed: activation readiness is judged once on the resulting document for every entry path (create-with-status and whole-document replacement included), and a checkpoint whose database write failed is reported as partial instead of as a clean record.
  • studyloop now is plan-aware. An active plan biases the recommendation — matching due work and a synthesised next milestone carry explicit plan and milestone references, urgent reviews and fresh struggles can still outrank new milestone work, several active plans are considered deterministically, and a milestone beyond today's energy is deferred with a reason rather than dropped. CLI now, Web Today, the recap and MCP get_next_action consume one additive result; with no active plan the JSON is byte-identical to the pinned golden. get_next_action gains interleave parity with the CLI.
  • Repair carries an energy demand of its own, and a body-doubling floor. A live struggle (struggling, seen within 14 days) asks for 6/10, an older struggle or a weak teach-back for 4/10, a concept still learning for none; below the day's capability a repair is deferred like new work into energy_deferred_repairs — never ranked — while due recall and teach-back reviews are never deferred. The due-review reader emits every struggling row as a hands-on "guided repair" as well, so that copy carries the same demand and defers with the repair, named once — without it the repair the learner had just been spared came back as the primary. When nothing plan-related fits the day and an active, ready plan exists, one low-scoring proposal is synthesised: sit with the plan in a body-double session (studyloop study "<plan>" --mode co-study), leading with the progress the plan records and naming what is deferred. Every offered command quotes learner-authored text as one shell word.
  • The low-energy teach-back is the one-sentence kind, with a way out. A concept still learning is offered as a micro teach-back (studyloop teachback … --type micro) and its reason reads as the gentle review it is, not "repair now"; the teach-back protocol gains a low-energy fallback — when the sentence will not come, the mentor gives a short guided explanation and asks for one phrase back, instead of walking the four-round stuck ladder, and the blank is not scored.
  • Plan with the architect from the Web UI. Plans → Plan with architect launches the Study Plan Architect through the ordinary session machinery (one-session authority, reconnect, the same console) with a planning purpose; starting a conversation creates no draft. The manual form remains. studyloop plan repair <id> opens the architect on an active plan the readiness gate refuses to write, and studyloop plan close <id> reviews a fully-checked plan against its evidence and proposes extend or close — status never changes by itself, and a partial review never proposes a clean close. A fully-checked plan that is not active can be closed or deleted; the architect asks which.
  • MCP lifecycle parity for Study Plans: list_study_plans, get_study_plan, get_planning_interview, create_study_plan, update_study_plan, set_study_plan_status, set_study_plan_milestone, evaluate_study_plan and delete_study_plan (confirmation required), thin adapters over the same seam; the mission is revisable through update_study_plan. studyloop doctor reports a plan document it cannot read by name instead of hiding it behind "all ready".

Changed

  • Semantic search is no longer opt-in on long-lived surfaces. Retrieval mode resolution is tri-state and per-surface: an explicit semantic_search.hybrid: true|false in the config file always wins; an absent key falls through to the surface default (resolve_mode(surface=…)), and the MCP and web surfaces now default to hybrid while the one-shot CLI stays lexical. The flip passed the owner-signed Stage 5 gate on the shipping system (30 starts × 100 requests per cell: p95 upper bound 77–187 ms against ≤ 200 ms, paired overhead 26–89 ms against ≤ 100 ms, first-query p95 618–740 ms against ≤ 3,500 ms; receipts under docs/architecture/session-memory/receipts/semantic-layer/), and is confirmed against the owner's one-shot SEALED run (G2 non-inferiority passed; G1's +0.05 lift was not established at n=84 — consistent with, but under-powered against, the census; the owner ruled to keep the default under the registration's re-open clause, receipt committed). The retrieval_status payload's field-level schema is now pinned by a contract test.
  • The query side of a hybrid search defaults to the ONNX encoder (semantic_search.query_encoder: auto — onnx where the model has a pinned artefact, torch otherwise; explicit torch/onnx always wins). Parity against the torch encoder passed on the full gold DEV set (91/91 identical ranked lists, cosine ≥ 0.999 per query); encoder load falls from ~2.9 s to ~0.2 s and a one-shot CLI hybrid search from ~2.9 s to ~0.33 s p50.

Fixed

  • The Study Plan Architect's launch from the Web UI waits at most 8 s for the session picker's options, so a stalled /api/session/options cannot hold a click forever; leaving the page before a requested launch lands leaves a session like any other (reattachable, ended with End) rather than a half-state.
  • A legacy study_progress row with no last_seen no longer crashes the struggle collector — and with it studyloop now — on the way to a recommendation.
  • The release gate (scripts/check-release-consistency.py --release) read a folded deferred: >- reason in an openspec change's .openspec.yaml as the literal marker >-, so it printed no reason and would have accepted an empty one — the unexplained deferral the guard exists to refuse. It now reads the indented text and refuses an empty block.
  • The Study Session view's timer ran its init() twice per page load (Alpine's own call plus the markup's x-init), each run reading /api/session/state. A tab sitting on the session picker could adopt a session started in another tab when the slower read landed — Start hidden under the live layout, no 409, no reattach offer. init() now runs once per page load; a second tab's Start reaches the server and is offered reattach. Named by the state captured at the click in CI (third occurrence, first with evidence) and pinned by a node --test replay of that race.

Security

  • The offered commands in studyloop now (evidence_command, the body-double door) quote every learner-authored value — plan titles, concepts, topics — as a single shell word. A plan titled SQL $(touch pwned) Windows used to run its substitution when the offered line was pasted into a shell.
  • anyio 4.12.1 → 4.15.1 for two published advisories (CVE-2026-63374, CVE-2026-64847); both dependency audits are clean again.

Removed

  • The forbidden-token test that failed the suite whenever "KiroCrew" appeared in a tracked file. It pinned the 0.3.0-era removal of the KiroCrew integration and made the removal impossible to even mention in docs or this changelog; the removal has shipped and the guard is retired (owner decision, 2026-09-15).

[0.4.0] - 2026-09-14

Added

  • The study-plan-architect is a first-class mentor role in every harness. One canonical persona (agents/shared/personas/plan-architect.md) is delivered by the launcher through studyloop study --mode plan-architect (or the studyloop plan architect alias) to Codex, pi and Grok Build, and installed as a native named agent for Claude Code, Kiro CLI and OpenCode by studyloop install agents; doctor reports the definitions; the installer's next steps point at it again.

  • OpenCode and Grok Build now get both StudyLoop MCP servers (session-db, studyloop) registered by studyloop install agents / doctor --fix: OpenCode globally in ~/.config/opencode/opencode.json and per session, Grok Build through grok mcp add (status read from grok mcp list --json, the legacy user-settings.json map honoured read-only). Server names are the same in every harness config; studyloop-mcp and session-db-mcp are the commands. The CLI fallback (session-query) stays documented for all six; pi remains CLI-only because it has no MCP support by design.

  • just release-check can run before the release tag exists: the release consistency check gained a --pre-tag mode that defers only the tag/date assertion, just release-verify runs the strict form after tagging, and a release note that is still the prepare-release skeleton now fails the gate.

  • Semantic session search. Schema 48 adds an embedding substrate to the session database (message_embeddings, chunked and content-hashed, with a sqlite-vec sidecar index); session-maint embed fills the backlog, session-maint embed-check [--fix] audits alignment, studyloop doctor reports embeddings_alignment, and the export hook keeps vectors current. Every agent-facing search (session_search over MCP, session-query, the retrieval half of memory_search) now goes through one retrieval.search service, so identical requests return identical ordered results. Natural language queries no longer crash on FTS5 syntax. Hybrid mode fuses the lexical and embedding arms with unweighted Reciprocal Rank Fusion; it is off by default and enabled with semantic_search.hybrid: true or, for one process, STUDYLOOP_RETRIEVAL_MODE=hybrid.

  • Doctor checks exporter_schema (the installed session-export agrees with the database schema) and export_freshness; session-export hooks are pinned to the installed binary by absolute path and log what they did.

  • The source install selects its Python deterministically and says so. A committed .python-version (3.12) drives uv sync/uv run; studyloop install tools passes the same minor to every uv tool install; ./scripts/install.sh prints the interpreter uv resolved, installs the pinned 3.12 through uv when no matching interpreter exists, and refuses anything outside 3.12–3.14; UV_PYTHON=3.13 ./scripts/install.sh is the documented override. The nightly install workflow now runs the real installer (a separate installer job) and checks Python 3.14 as a signal; CI asserts each matrix job runs the Python it names.

  • obsidian.filename_template is honoured by the Obsidian export (it was documented but never read).

  • scripts/check-release-consistency.py --release also requires the version's git tag to exist and the CHANGELOG heading date to be no earlier than it.

  • Grok Build sessions export automatically at session end. studyloop install agents / studyloop doctor --fix write ~/.grok/hooks/studyloop.json (under GROK_HOME when set), a SessionEnd hook running session-export --grok-only that skips a subagent's own teardown and never fails the hook, plus the session-memory mandate in ~/.grok/rules/session-db.md. Doctor reports session_export_hook_grok; the shared AGENTS.md wind-down step now names the Grok flag beside Codex's.

Fixed

  • Export hooks and doctor's exporter_schema check follow uv's real tool bin directory (UV_TOOL_BIN_DIR, else uv tool dir --bin) instead of assuming ~/.local/bin; with the default directory the portable $HOME/.local/bin form is unchanged. Found by the first run of the uv tool install smoke inside just release-check. The smoke itself now pins the interpreter from .python-version like the installer.

  • Installing from source no longer depends on whichever python3 happens to be on PATH: the installer used to validate that interpreter and then let uv pick a different one, and uv tool install resolved without any --python at all (reported by a user who needed UV_PYTHON=3.12). See "Added".

  • The documentation and the code now agree on the harness contract: six first-party mentor harnesses everywhere (Kiro CLI, Codex and Claude Code core; OpenCode, pi and Grok Build preview), session-export --help lists the Grok source, the mentor install guide detects grok, and the architecture notes state which harnesses register MCP servers (Claude Code, Kiro CLI, Codex) and which speak ACP (Kiro CLI, Grok Build). CONTRIBUTING.md and SECURITY.md no longer name a version line that goes stale. Reviewed against the OKF removal: no okf/ontolog code remains and no reference to Kiro's crew tooling (never a StudyLoop harness) remains; tests now lock both out.

  • Session-memory documentation describes the shipped database: schema 48, the embedding substrate, the retrieval modes and their precedence, and the session-maint embed/embed-check commands. The semantic_search example no longer lists fts_weight/semantic_weight (removed from the defaults as well: fusion is unweighted), the session_search MCP docstring names both retrieval modes, and the four copies of an obsolete "schema-30" warning are gone.

  • Configuration keys behave as documented. agent-session-tools honours a top-level session_db: with the same precedence as studyloop, so both packages open the same database; studyloop doctor no longer flags the database, hosts, semantic_search, thresholds, logging and excluded_dirs sections the setup guide recommends as unknown; studyloop setup puts the chosen harness first in agents.priority instead of writing a dead ai_assistant key; the Obsidian doctor check verifies the memory directory is writable; the undocumented tts.pause key and the false tmux-resurrect doctor claim are removed from the setup guide.

  • Installed mentor prompts only instruct commands and tools that exist: the phantom tutor-progress command is replaced by the real tutor-checkpoint <skill> --notes; the Claude Code status line renders the energy label; pi and OpenCode prompts fall back to session-query where the session-db MCP server is not registered; the MCP README says the installer writes Codex's config; the orphaned, wrong-schema agents/opencode/mcp.json is deleted; the installer's next steps no longer name the unshipped study-plan-architect agent.

  • Architecture and reference docs match the tree: file maps point at the real modules, the deleted "Stub" card generator is gone from the backend lists, dead links are fixed, the TUI guide's timer numbers come from the code, the first-week extract-struggles example passes the required --harness, the CLI reference documents session-maint embed/embed-check and the doctor exit codes accurately, and the study-plans guide acknowledges the record_plan_learning MCP tool.

  • Developer tooling: the Claude Code openspec-gate hook resolves its script through ${CLAUDE_PROJECT_DIR} (a cd in the session used to break every shell call), the local ci-standards bandit skip list matches CI, and just release-check runs the uv tool install smoke script.

  • studyloop doctor no longer crashes on a Kiro CLI that has migrated the study-mentor agent file. The shipped template writes {"hooks": {"stop": [...]}}; a migrated file writes {"hooks": [{"trigger": "stop", "action": {...}}]}, and the Kiro hook check assumed the first shape. It now reads either, so the session-export hook is still reported present, canonical or legacy whichever schema the installed Kiro CLI uses.

  • A fresh install can start a session and call every memory tool from its first run, instead of hitting an unhandled traceback or a bare sqlite error. Both packages' config writers now write memory.default_scope: unclassified explicitly for a brand-new config.yaml (the runtime default when no config exists at all, or when an existing file omits the key, stays intentionally unset). Every remaining case where scope is genuinely unconfigured — the studyloop CLI (now exits 2), all seven previously-unguarded MCP tool call sites, and session-db-mcp's open_context() on a database that does not exist yet — now reports one structured {code: "scope_unconfigured", message, remediation} diagnostic instead of a crash or an ad-hoc error shape.

  • Re-exporting a touched OpenCode session can no longer destroy conversation history. OpenCode rewrites time.updated on any touch and flushes its message/part files asynchronously, so a re-export can legitimately read nothing; the importer deleted the previously captured rows before it knew the replacement set was non-empty, turning that ordinary race into permanent loss of the only surviving copy of a session. It now collects first and, like every other importer, hands replacement to the shared commit path, which removes only empty stale rows, refuses to drop a message any evidence row still cites, and files edited text as a revision instead of overwriting the original.

  • One bad batch can no longer abandon the rest of an export run. Each importer's leftover partial batch was committed outside its per-source error guard, and the OpenCode and Kiro importers had no guard around their mid-loop commits either — so a single collision or transient database error propagated out of the importer, and because sources are exported in sequence with no guard of their own, every later source was skipped. Any source with fewer than fifty sessions has only a final batch, so for small harnesses nothing was contained at all. Every batch commit now records the failure and continues; the database is still rolled back exactly as before.

[0.3.0] - 2026-09-06

Added

  • The web app's Today panel can now launch your configured second brain with one explicit click. A read-only GET /api/second-brain/launch-target route reports the selected provider's honest launch state (never cached, never a configuration error's detail); the Today panel renders at most one launcher action from that prefetched state and navigates only inside the click handler — xTiles opens once in a new noopener,noreferrer tab, Obsidian hands the current tab to the obsidian:// link. Nothing navigates during page load, refresh, publication, or wind-down, and no server module launches anything, redirects, or contacts a provider for it.
  • studyloop brain destination set --provider xtiles --url URL (and ... destination clear) retains a reviewed, assistant-supplied xTiles page URL without changing provider consent. The URL is validated at the configuration boundary — HTTPS, exactly xtiles.app or app.xtiles.app, a real page path, no userinfo, port, query, or fragment, Unicode/IDNA lookalikes rejected — and a rejected value is reported by reason category only, never echoed. Confirmation and Settings show at most the host, never the full retained URL.
  • Obsidian launch targets resolve to <vault>/<folder>/Today.md when that note exists inside the vault, falling back to the vault root, and are enabled only when the browser is on the device running StudyLoop (decided from the direct request peer; behind a reverse proxy the action stays safely disabled).
  • Settings renders one card per second-brain provider — active for the selected provider, muted otherwise — with the launch API's own reason as guidance and the CLI command that would select a muted provider. No web form writes configuration.
  • studyloop.settings.mutate_raw_config() is now the single atomic read-modify-write owner for configuration mutation: an exclusive sibling lock, a reread after locking, whole-config validation, a synced 0600 temporary sibling, and an atomic replace that preserves the destination's mode. brain enable and both destination commands go through it, so two concurrent StudyLoop writers can no longer silently lose each other's update.
  • An installed-wheel web smoke (just smoke-web, part of just release-check) builds the wheel, installs studyloop[web] into an isolated environment outside the checkout, starts the installed application, requests the launch-target route, and loads every launcher asset reachable from main.js's import graph — so a packaging regression in the launcher cannot ship invisibly again.
  • A session-memory reliability candidate: session-repair inspects existing imports and offers explicit, backed-up repair; session-sync all reconciles configured peers by pushing to every peer before pulling from every peer. Release acceptance is pending. The candidate retains the existing trusted whole-database transport, without enforced work/personal scope, propagated forgetting or validated decision arbitration.
  • Grok local transcript import through session-export --grok-only, separate from StudyLoop's supported mentor and automatic-hook integrations.
  • A conversation memory guide covering repair, verification, project-filter limits and permitted database sharing.
  • Session memory is now an installer/doctor invariant across all five release harnesses. studyloop install agents installs one studyloop-session-memory skill (MCP-first query with session-query fallback) and a real automatic export hook for Kiro (stop), Codex (SessionEnd), Claude Code (Stop), OpenCode (session.idle) and pi (session_shutdown). Doctor checks the query/export executables, each harness's reachable skill, steering mandate and native hook separately; doctor --fix uses the same top-level installer rather than a drifting partial repair path. Codex's global hook also covers Codex app sessions that load ~/.codex/hooks.json; consumer Claude Desktop remains explicitly unverified rather than being conflated with Claude Code.

Changed

  • Topic exercises are now an explicit developer preview rather than part of the supported learner surface. Their value alongside the mentor's existing Socratic workflow is still unproven, so normal CLI help, MCP discovery and the production web API no longer expose them. Opt in deliberately with studyloop --dev exercise …, an MCP config that launches studyloop-mcp --dev, or studyloop web --dev for the HTTP API. Removing the flag removes the surface entirely (unknown CLI command, no exercise_* tools in tools/list, no /api/exercises routes) rather than leaving a registered command that fails at runtime. The implementation and documents remain available for evaluation and can return to the supported surface if learner demand demonstrates value.

Fixed

  • Conversation import and repair preserve stable message identities, retained history and local annotations; changed Codex, Claude Code and Kiro transcripts can be revisited without discarding unrelated history.
  • Sync reconciliation revisits shared sessions even when timestamps have not changed, preserves different nonempty message content and archives conflicting learner-row variants. SSH runs unattended with bounded connection attempts, and a failed peer does not prevent attempts to other configured peers.
  • Secret-store tests isolate provider fallback variables as well as their temporary configuration, preventing ambient credentials from entering the observed failure assertions.
  • Release validation now runs without the three warning classes found during the 0.3.0 readiness pass: hook JSON merges no longer use an exists()-then-read_text() TOCTOU shape; Starlette TestClient uses the exact-pinned httpx2==2.12.0 successor package; and the real PTY transport tests scope Python 3.13's multithreaded forkpty() warning to that one intentional test module. PyMuPDF's known SWIG __module__ deprecation is handled at its import boundary and its surviving SWIG types are labelled, preventing both import-time and interpreter-shutdown warnings without a global pytest filter.

[0.2.1] - 2026-09-05

Added

  • studyloop plan record <plan-id> --title <title> [--body … | --body-file …] appends a learning record to a plan document, with a matching MCP tool (record_plan_learning) so a mentor can record what was learned at wind-down. Before this, a learning record existed only if you typed it into the plan by hand — and an xTiles wind-down's record lived only in xTiles. The write goes through the plan renderer (never appended as raw text), numbering continues from the highest existing record, and re-running with the same title and body changes nothing. The xTiles wind-down now records into the plan first, so the xTiles page is a projection of a record the plan already has. ADR-0010 was amended to state the rule the code obeys: studyloop plan … is the plan document's only writer.
  • studyloop brain wind-down --json [--connector NAME]... answers the one question the end-of-session protocol needs: which second-brain offer to make, if any. It returns the channel, whether to offer, the exact sentence to say, and why — so an agent no longer derives the decision from flags, and the offer sentence cannot drift between the CLI, the protocol, the skill and the guide (they are pinned byte-identical by tests).

Changed

  • The xTiles guidance now matches what a person actually saw running the three prompts end to end (Kiro CLI 2.21.0, 2026-09-04). The planner prompt creates a planner tile and reports its URL — the one shape the live UI check validates. The project prompt no longer promises a Kanban board or refreshing collection pages: the connector cannot create board views and refuses to patch collection pages on any tier. The wind-down prompt now says to skip the Review task when get_due_cards returns nothing due, rather than inventing a date. The guide also now says that the next action and due reviews sent to xTiles reflect your whole study history (they are not plan-scoped), that per-write permission prompts are xTiles' statement about its connector and what you see depends on your assistant, that the xTiles learning outcome is recorded in the source plan before the assistant projects it into xTiles, that StudyLoop itself still performs no xTiles network call, and that planner tiles can be removed through the connector while pages and projects must be deleted in the xTiles interface by hand.
  • studyloop install agents no longer links the xTiles wind-down skill into ~/.config/opencode/skills: OpenCode already lists the shared skills hub as a global search path, so the extra link risked a duplicate listing.

Fixed

  • The Obsidian publisher closed three review residuals: containment is now checked before any directory is created (a hostile ancestor symlink can no longer cause directories outside the vault) and once more immediately before the atomic replace; --dry-run now says "would replace your edits in …" — with the same warning a real publish prints — instead of a plain "would write"; and a note whose permissions cannot be read is refused rather than silently rewritten with default permissions.
  • get_study_history (the MCP tool) and studyloop plan evaluate no longer fail when searching session history: the full-text query joined two tables that both carry a content column without qualifying it, and OR'd multiple MATCH constraints, which FTS5 refuses. Multi-word topics such as "window functions" now also match as phrases rather than scattered terms.

[0.2.0] - 2026-09-04

Added

  • An optional second-brain layer: studyloop brain status reports whether a provider is configured, and studyloop brain publish --plan <id> publishes a projection of that plan. Nothing is written anywhere until you opt in with a second_brain: section in config.yaml, and nothing ever writes back to the plan document.
  • Publish your study plans, today's next action and due reviews into an Obsidian vault as safe, idempotent notes StudyLoop owns. Your own notes are never overwritten: StudyLoop writes only files carrying its own marker, only inside the folder you name, never through a symbolic link, and it reads your personal notes for a plan only when you ask it to with studyloop brain pull. Republishing an unchanged plan writes nothing at all. Nothing in this feature runs an external program or stores a credential.
  • An Obsidian study-plan template ships with StudyLoop (studyloop brain template --install); it mirrors the plan document's sections, so a plan you write by hand in your vault and one StudyLoop published look the same. Templates carry no ownership marker, so a note you make from one is yours.
  • studyloop brain publishes plans and today's study to a configured second brain, pulls your notes back on request, and studyloop config init can enable it when you point StudyLoop at a vault. studyloop doctor reports the provider, the vault and whether that vault is writable — but only if you have configured one.
  • The project website, www.studyloop.dev, is linked from the README and the documentation home.
  • xTiles users can move today's study and their whole plan into xTiles through their assistant's MCP connector: the Second Brain guide ships the three prompts, and studyloop install agents installs an opt-in wind-down skill into every harness it detects. The skill is silent unless your provider is xtiles and an xtiles server is connected, and StudyLoop stores no xTiles credential.

Changed

  • The wind-down protocol offers, once, to publish the session to your second brain when one is configured that can be published to. It says nothing otherwise.

Notes for anyone who ran a pre-release build

  • An optional Obsidian CLI adapter existed during development and was withdrawn before release, after a multi-model review found that it could write into a vault other than the one you configured, and that it passed your plan text as a command-line argument where other users on the machine could read it. StudyLoop writes notes directly instead, which is all this feature ever needed.
  • The keys use_cli, vault_name, template and daily_note are gone. If any of them is still in your config.yaml, StudyLoop now says so and stops rather than ignoring it — daily_note used to append a line to your own daily note, and you should know that it no longer does.

[0.1.0] - 2026-09-03

Added

  • A learner-focused Web UI for starting Study Sessions, Body Double sessions, reviews, and study-plan work.
  • First-party harness support for Kiro CLI, Codex, and Claude Code.
  • Preview harness support for OpenCode and pi.
  • Session-history import for each supported harness.
  • Study-plan creation in the Web UI (Study Plans → New plan) and the CLI. The Web UI form is manual, with seed suggestions drawn from your own history; an agent-led planning interview is not integrated there yet.
  • AuDHD-aware session scaffolding, spaced review, teach-back, parking-lot, and wind-down workflows.
  • A strict public-documentation build and a separate internal audit/archive area.
  • SECURITY.md: how to report a vulnerability, the supported-version policy (0.1.x only, pre-1.0), and a two-sentence summary of the LAN-password and encrypted-secrets-store threat model.
  • THIRD-PARTY-NOTICES.md now credits all 26 vendored web assets under web/static/vendor/ — four SIL OFL font families (Atkinson Hyperlegible, Inter, Lexend, OpenDyslexic) and eight JavaScript libraries (Alpine.js, htmx + the SSE extension, marked, DOMPurify, highlight.js, mermaid, Fuse.js, xterm.js + its three addons) — not just the one design-influence credit it previously had.
  • web/static/vendor/MANIFEST: upstream URL, version, and sha256 for every vendored web asset, checked by a test that recomputes each hash and fails on a mismatch or an unlisted file. Every non-local URL was re-fetched and byte-compared while writing this — which also confirms two things a prior review flagged as unverified: htmx's current licence is Zero-Clause BSD (not BSD-2-Clause), and the vendored xterm.js "6.0.0" is a real upstream tag, not a typo.

Changed

  • Reworked the README and public guide around learner outcomes and approachable setup, with a real Kiro CLI walkthrough captured from the Web UI.
  • Made the supported harness contract explicit across setup, diagnostics, launching, session export, and documentation.
  • Made struggle extraction require an explicitly selected live model and a real exported harness session.
  • Added source-session provenance and transaction-safe writes to struggle extraction.
  • practice verify --run-command now requires --yes (or an interactive y at the prompt) in addition to --run-command before it actually executes a practice deck's verification command; the resolved command is always printed first. Without confirmation, nothing runs and the command exits 2.
  • mcp[cli] is now pinned >=1.0.0,<2 everywhere it is declared, so an unlocked install (a future pip install/uv tool install outside this repo's lockfile) can no longer select the 2.x release that renamed FastMCP and breaks studyloop-mcp/session-db-mcp at import.
  • fastmcp (agent-session-tools) is now pinned >=3.1.1,<4: an unlocked install previously resolved fastmcp 4.x, which transitively pulled in the same broken mcp 2.x release the pin above already guards against.
  • The workspace-root pyproject.toml version now matches packages/studyloop/pyproject.toml's (both 0.1.0); the release-consistency check now fails if the two ever disagree again.
  • Removed the studyloop[sessions] extra and dropped sessions from studyloop[all]: agent-session-tools is not published, so it can never resolve as a wheel extra outside this repo's own workspace, and the extra was never actually what delivered it into the CLI tool venv anyway -- studyloop install tools/./scripts/install.sh already add it unconditionally, independent of any extra. studyloop[all] still expands to content, bedrock, notebooklm, tui, web, and mcp; every one of those (plus all itself) is now proven, per-extra, to install and import from a bare built wheel with no workspace present.

Removed

  • Product-selectable fake agent and deterministic content backends. Test fixtures now live only in the test suite and are excluded from distributed packages.
  • First-party session-harness claims for Gemini CLI, Antigravity, Grok, and local-model launchers.
  • Session exporters outside the five-harness pre-release contract.
  • The 90-day study heatmap from the Flashcards panel. It was a placeholder that always rendered 90 empty cells with no real per-day data behind it — advertised as a working feature while doing nothing. Will return in 0.2.0 wired to real review counts, under the asymmetric-visibility design in ADR-0006.
  • Public pages that exposed implementation notes, internal architecture detail, or release-planning material.
  • ttyd entirely — the browser terminal fallback and the server transport that backed it. studyloop study --transport, studyloop web --ttyd-port, the ttyd_port config key, the /terminal/ HTTP+WebSocket proxy, and the transport: "ttyd" / STUDYLOOP_TRANSPORT=ttyd request paths are all removed; requesting ttyd now fails with a clear error instead of being silently downgraded to another transport. studyloop study is unaffected — it never depended on ttyd to work. See ADR-0008.

Fixed

  • THIRD-PARTY-NOTICES.md linked Apache-2.0 and MPL-2.0 (DOMPurify, Fuse.js) instead of reproducing them; both licences require the licence text to accompany redistribution (Apache-2.0 §4, MPL-2.0 §3.1). Both are now reproduced in full; confirmed neither project ships an upstream NOTICE file at its pinned version.
  • THIRD-PARTY-NOTICES.md said vendor/ "ships 26 third-party files"; four of those 26 are first-party @font-face glue CSS sheets this repo wrote itself, not third-party. Split into 22 genuinely third-party plus 4 named first-party files.
  • Ending a Study Session or Body Double session used to close the confirmation dialog and tear down the session state before the /api/session/end request even resolved, so a failed request looked identical to a successful one — the setup guide says ending from here is what saves the summary and evidence. The dialog now stays open with an error message and a working retry (the same button) until the server actually confirms the end. The two "no confirmation, resolve a blocking session immediately" paths keep their existing behaviour (tear down regardless — a stuck banner is worse) but now show a toast on failure instead of staying silent. Flashcard/quiz review recording (/api/review) also now shows a toast on failure instead of silently discarding it.
  • docs/first-week.md said "All eight agent CLIs"; the pre-release contract is five (Kiro CLI, Codex, Claude Code as core; OpenCode and pi as preview). Corrected.
  • The "Web PWA" section of docs/cli-reference.md is now "Web UI" and states the app is installable but does not work offline, matching the qualifier already used on docs/index.md/docs/web-ui-guide.md.
  • The study-plan brain-dump field's hint claimed "the study-plan agent can turn it into the fields below" — no agent decomposition is wired; submitPlan() keeps the text verbatim. Hint now says so.
  • docs/agent-install.md's OpenCode section conflated two different mechanisms under one "installs" narrative: the global studyloop install agents --tool opencode write to ~/.config/opencode/agents/, and the separate project-local .opencode/agents/study-mentor.md + .opencode/opencode.json that studyloop study --agent opencode writes at session start. Split into two clearly-labelled paragraphs.
  • releases/v0.1.0.md hardcoded a numeric test-total claim — a number that goes stale the moment the suite grows — in direct violation of the rule its own sibling public page, CONTRIBUTING.md, states in the imperative. Replaced with a pointer to the release-gate evidence.
  • docs/cli-reference.md now documents the 8 content subcommands that manage NotebookLM notebooks/podcast syllabi (autopilot, delete, download, generate, list, status, syllabus) and the fast content index (index), plus studyloop topic (singular) — the command AI agents run every session to log activity, which had no public reference entry at all. docs/content-pipeline.md gained a short table for the NotebookLM subcommands and a paragraph on content index.
  • studyloop doctor --category updates was an advertised, --help-listed choice that always produced zero results (nothing is published, so its one check is deliberately unregistered) and an unreachable --fix branch. Removed from the CLI's category choices until a release exists to check against; docs/cli-reference.md's category list updated to match, and now mentions the new tmux check under core.
  • The error studyloop install tools/agents and doctor --fix raise outside a source checkout named a pre-studyctl-rename project ("socratic-study-mentor") instead of studyloop.
  • studyloop doctor now checks for tmux (3.1+, studyloop study's hard dependency) under the core category, with an install hint, instead of giving no signal at all before a first studyloop study fails outright.
  • A bad scalar config value (e.g. web_port: "abc") now raises a one-line actionable error naming the key, the file, and the bad value, instead of an unhandled Python traceback. studyloop config show now also warns about unknown or retired top-level config keys (a misspelled key, or one left over from a removed feature) — doctor already did.
  • docs/tui-guide.md's break-schedule table disagreed with the actual energy-adaptive thresholds in three numbers and both the Medium/High band boundaries (doc: Medium 4-7 = 20/45/90, High 8-10 = 25/50/120; code: Medium 4-6 = 20/40/75, High 7-10 = 25/50/90). Table corrected to match logic/break_logic.py.
  • studyloop backlog add/list/resolve --help told you to run studyloop topics ... — a different real command that rejects those arguments. The examples now say studyloop backlog ..., matching suggest's docstring, which was already correct.
  • The TUI sidebar's +5min focus key was dead: the binding was registered as "plus_sign", but Textual's real name for the + key is "plus", so pressing + never fired. Fixed; docs/tui-guide.md's claim that + lengthens the Pomodoro focus period is now true.
  • session-query list --since 7d on docs/setup-guide.md and docs/cli-reference.md raised an unhandled traceback if pasted verbatim — query_utils.parse_date only accepts YYYY-MM-DD or last-N-days. Both pages now show --since last-7-days, matching the fix already applied to docs/first-week.md.
  • docs/setup-guide.md's "Interactive Setup" section described studyloop config init's three questions (knowledge bridging, a "study material location" question setup never asks, an Obsidian vault path) under the studyloop setup heading. It now describes setup's real, shorter flow (notes folder, optional focus-topic confirmation, optional harness pick) and correctly separates out config init's own three questions instead of conflating the two commands.
  • Removed the dangling GEMINI.md symlink at the repo root (it pointed at agents/gemini/GEMINI.md, which no longer exists now that Gemini CLI is not a supported mentor harness). A guard test now fails the suite if any tracked symlink ever points at a missing target again.
  • Cross-machine sync (session-sync push/pull/sync) no longer overwrites global tables (study_progress, study_sessions, teach_back_scores, knowledge_bridges, concepts, concept_aliases, concept_relations, message_concepts, parked_topics, scrub_log) unconditionally. Every row is now gated on updated_at, matching the check already applied to sessions/messages, so a stale machine's dump can no longer silently revert a newer board move, teach-back score, or progress row on the receiving side. push and the remote side of sync now back up the destination before writing to it, exactly as pull already backs up its own destination.
  • Session archive/delete/prune cutoffs (session-maint archive/delete-old, tiering's prune/refocus) and focus suggestions computed their cutoff from naive local wall-clock time, then compared it against UTC-sourced timestamps. On a machine east of UTC this could delete sessions updated within the last few hours; west of UTC it could keep sessions that should have been caught. Cutoffs now use real UTC.
  • record_teachback's write to study_progress now runs inside an explicit db.immediate() transaction instead of relying on statement order to hold SQLite's write lock; a CHECK-constraint violation (an out-of-range teach-back score) no longer raises unhandled through record_teachback; and the study_progress row id derived from (topic, concept) is now separator-safe, so a topic and concept that together contain a : can no longer collide with a different pair.
  • Roughly 30 read/write helpers across history/{sessions,progress,bridges, streaks,teachback}.py and learning/mastery.py caught every sqlite3.OperationalError the same way, including a genuine lock/timeout fault — silently returning "no wins" / "no struggling topics" / "no progress" instead of surfacing the failure. Narrowed to the specific "no such table" case (an expected, pre-migration schema gap); anything else is now logged and re-raised, matching the fix already applied to the explorer search path.
  • review_db.py's six public functions (ensure_tables, record_card_review, record_session, get_due_cards, get_wrong_hashes, get_course_stats) used with conn: for cleanup, which only commits or rolls back a transaction — it does not close the connection. Every flashcard/quiz answer (POST /api/review) leaked a sqlite3.Connection, relying on CPython's refcounting to eventually close the file handle. Now try/finally: conn.close(), matching the convention used everywhere else in the codebase (parking.py, notes.py, history/*.py).
  • The tmux/herdr integration test harness (tests/harness/multiplexer.py, tests/harness/study.py, tests/harness/terminal.py, tests/harness/agents.py) and the test modules that drive it (test_harness_matrix.py, test_study_integration.py, test_uat_terminal.py, test_study_lifecycle.py, test_herdr_integration.py, conftest.py) hardcoded ~/.config/studyloop for session-state IPC files and spawned the CLI without setting STUDYLOOP_SESSION_DIR. Running these suites locally read, wrote, and deleted the developer's real live session state. Every path now derives from STUDYLOOP_SESSION_DIR, redirected to a tmp_path-based directory by each fixture, and forwarded to every spawned studyloop subprocess.
  • Cross-machine sync's recency gate (excluded.updated_at > table.updated_at) was NULL-falsy: a destination row with a NULL updated_at could never be overwritten by any incoming row, however new, freezing it forever. Every study_sessions row created after the R-19 migration was silently one of these, since that table's updated_at had no default and nothing wrote it. Now COALESCE(destination.updated_at, '') treats a NULL destination as older than anything, and a trigger stamps updated_at on every insert and update to the tables that previously had no default for it.
  • Cross-machine sync used id INTEGER PRIMARY KEY AUTOINCREMENT as the conflict target for teach_back_scores, knowledge_bridges, parked_topics, and scrub_log -- a counter private to each machine, starting at 1 independently everywhere. Two machines' different row #1s collided as the same row, and the incoming row was silently dropped (no error, row count unchanged). These four now use a migration-backfilled sync_key; concept_relations (which already had a real natural key) now syncs on that instead of its own autoincrement id.
  • Cross-machine sync's remote backup (push/the remote side of sync) and the local session-maint backup helper both copied the database file with a plain file copy (cp -p over SSH, shutil.copy2 locally). In WAL mode, data committed but not yet checkpointed into the main .db file lives in the sibling -wal file, which a plain file copy never reads — the backup silently missed recently-committed rows. Both now use a WAL-aware backup mechanism (the sqlite3 CLI's .backup dot-command remotely, sqlite3.Connection.backup() locally).
  • push and the remote-writing step of sync discarded the result of backing up the remote destination — a failed backup was logged and the write proceeded anyway, unprotected. Both now abort the write when the backup fails. The remote backup also had no retention: every push/ sync call left another .bak-<timestamp> copy next to the remote database forever. It now rotates, keeping only the newest 5 (matching the local backup helper's own default).
  • Cross-machine sync's global-table recency gate compared updated_at as a raw string. Every current writer produces the same canonical format, so this was not a live bug today, but a bare string compare is silently wrong across formats that place a different character at the same position (for example, a T-separated timestamp sorts as "later" than a space-separated one at the same clock time, regardless of which is actually later) — exactly the kind of mismatch real elsewhere in this codebase's session-import exporters. The gate now compares both sides through SQLite's datetime(), so it stays correct if a future writer, a manual edit, or an older/foreign row ever disagrees with today's uniform writers.
  • record_teachback caught every sqlite3.DatabaseError the same way, including a genuine lock/timeout OperationalError -- indistinguishable from an expected CHECK-constraint rejection (an out-of-range teach-back score), both silently returning "not recorded." Narrowed to sqlite3.IntegrityError for the CHECK-violation case; anything else is now logged and re-raised unless it is a missing-table error, matching the fix already applied to the read-side helpers across history/*.py.
  • history/concepts.py's list_concepts and history/search.py's topic_frequency/struggle_topics had the same bare except sqlite3.OperationalError: return [] the rest of history/*.py was already fixed for -- a lock/timeout fault read back indistinguishably from "no concepts"/"topic never mentioned." Narrowed to the missing-table case; anything else is now logged and re-raised.
  • Starting a web Study Session or Body Double no longer clobbers a session already running in a terminal (studyloop study), and vice versa. The web UI's start path now checks the same cross-process session claim the CLI writes, refusing with a clear "already active" message instead of silently overwriting the shared session state and orphaning the CLI's running agent. A session left behind by a crashed process is still reclaimed automatically rather than blocking forever.
  • studyloop study no longer refuses to start after a crashed session left a stale claim; the stale claim is reclaimed with a logged warning (R-01b).
  • Reclaiming a crashed session's slot (web or CLI) no longer shows its topics-covered list and parking lot to the new session; both are cleared before the new session starts, not just touched (C2).
  • A second web server process (a different port, or a restart racing the previous one before it fully exits) starting a Study Session or Body Double now correctly refuses with "already active" instead of treating the first server's still-live session as stale (C3).
  • The study sidebar's IPC file poll no longer risks crashing its background thread if a session ends (and its state files are removed) at the exact moment the poll runs; it now reads first and treats a vanished file as "no update" instead of checking existence then reading as two separate steps (C7).
  • A reclaimed session's log line now names the crashed session's agent process (if one was recorded and is still running) so it is visible that an orphaned agent may still be alive on the machine; StudyLoop still never signals it (C4).
  • Closed a narrow race where a session start (CLI or web) checked whether a slot was free and only claimed it afterward, with real work (spawning the agent, creating the database record) running in between; a second start landing in that window could have raced the eventual claim. The check and the claim now happen together (C1).
  • Reclaiming a crashed session's slot could leave that dead session's multiplexer name and other identifying details attached to the brand new session that reclaimed it -- in the worst case, ending the new session could then affect an unrelated terminal session that happened to still be using that name. A reclaimed slot now starts completely clean (C10).
  • Ending a session -- from the Web UI, studyloop study --end, or the study sidebar's End Session key -- no longer terminates every other study session on the machine. Each end path now closes only its own terminal session (a web Study Session or Body Double closes none, since it owns no terminal). studyloop clean --all is the one place left to deliberately sweep every study-* session at once, for the rare case that's actually wanted.
  • The live session dashboard's activity stream (SSE) could very rarely drop its connection with no explanation if a session ended at the exact moment the stream polled for changes. It now tolerates that race the same way the rest of the session-state readers already did.
  • When a Kiro (ACP) session fails to start, the server log now includes whatever the agent printed before it crashed, instead of only a generic handshake-timeout message.

Security

  • The Claude adapter's launch command (executed via /bin/sh -c) is now built with shlex.quote() around both the binary path and the persona file path. Neither is attacker-controlled in the normal flow, but the persona path comes from tempfile.mkstemp() under $TMPDIR, which can contain a space — previously breaking the command outright rather than posing an injection risk, but unquoted shell interpolation is the wrong shape regardless of today's inputs.
  • Closed a gap where a .env file planted in or above the directory studyloop is run from could set the test-only STUDYLOOP_TEST_AGENT_CMD / STUDYLOOP_TEST_ACP_CMD hatch and get an attacker-chosen shell command executed on the next session start. The hatch is now honoured only when exported in the real process environment (as the e2e harness already does); a value that arrives via the .env auto-loader is deleted and logged.
  • Closed a second, independent gap that bypassed the fix above: a .env at ~/.config/studyloop/.env reintroduced the same test hatch at every web-server startup, via a second dotenv loader in agent-session-tools that ran after the first fix's scrub. Same rule, same guard, now applied in both loaders.
  • Hardened the fix above further: every production site that reads the test hatch now consults a value captured once, at import time, before any .env file is loaded, rather than re-reading the environment on every session start. A .env loaded by something other than this package, later in the process's life, can no longer set the hatch either.
  • practice verify --run-command now runs the EXACT command string a human confirmed, and refuses if the practice deck's command changed between being shown for confirmation and being executed, closing a time-of-check- to-time-of-use window in the R-15 fix above.
  • Closed a gap in the agent-child credential scrub where two credential- shaped words joined with no separator (SERVICE_APIKEY, SESSIONCOOKIE) escaped every existing pattern, which is underscore-anchored throughout.
  • Closed a gap in the agent-child credential scrub: a bare _KEY suffix (ENCRYPTION_KEY, SIGNING_KEY, MASTER_KEY, ...) and bare AUTHORIZATION/JWT/COOKIE-shaped variables now get stripped from an agent child's environment, matching the compounds (api_key, secret_key, ...) already covered.
  • config.yaml (which can hold lan_password in plaintext) is now written 0600 on every save, repairing a pre-existing 0644 file on its next save; a newly created config directory is created 0700.
  • Disabled the last piece of the Web UI's auto-docs surface: /openapi.json now 404s, matching the already-disabled /docs and /redoc.
  • practice verify --run-command no longer runs a practice deck's verification command blind: it is shown before it runs and requires explicit human confirmation (see ### Changed). The three local card-generation prompts (flashcard, quiz, practice) now instruct the model to treat the source material it is given as data, not as instructions to follow, and the practice prompt additionally warns against copying a command straight out of the source into a task's verification metadata.

Known pre-release boundaries

  • Kiro CLI is the documented demonstration harness.
  • OpenCode and pi are preview integrations while their release evidence is completed on supported local installations.
  • Grok may be used as an independent review model through the development gateway; that does not make Grok a StudyLoop code harness.
  • Live extraction does not silently fall back to sample output. If a model, credentials, session, or harness binary is unavailable, the command stops with an actionable error.

See the 0.1.0 release note for the current acceptance boundary. Earlier private development history remains available in Git rather than being presented as shipped product history.