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.
- The
nowengine'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. CLInowprintsFirst 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-notesAgent 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 bystudyloop 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_teachbackMCP tool records a teach-back score through the same validator thestudyloop teachbackCLI 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. Oneagents/shared/recording-protocol.mdcarries 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).
- 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 togit statusand skipped bygit add -A. Theherdr-ghostty-multiplexer-transportchange 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), soopenspec validate --archived --allis green for the first time since the release guard was added.
- The
nowengine 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 insidehistory/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 receiptnow-rubric-2026-09-16printed 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-onlynowfor 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.
- An acceptance-test tier modelled on the Terraform provider's
make testacc: every test underpackages/studyloop/tests/acceptance/skips with a named reason unlessSTUDYLOOP_ACC=1;just testaccselects harnesses (STUDYLOOP_ACC_HARNESS), actors and-kexpressions; 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 additionalSTUDYLOOP_UAT=1. Seedocs/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), andharness(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-encoderfetches and sha256-verifies the pinned ONNX query-encoder artefact (explicit action only — searches and hooks never download), andstudyloop doctorgains aquery_encoder_artefactcheck whose--fixruns 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 ONNXbge-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
PlanApplicationseam (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 nowis 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. CLInow, Web Today, the recap and MCPget_next_actionconsume one additive result; with no active plan the JSON is byte-identical to the pinned golden.get_next_actiongainsinterleaveparity 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 stilllearningfor none; below the day's capability a repair is deferred like new work intoenergy_deferred_repairs— never ranked — while due recall and teach-back reviews are never deferred. The due-review reader emits everystrugglingrow 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
learningis 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
planningpurpose; 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, andstudyloop 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_plananddelete_study_plan(confirmation required), thin adapters over the same seam; the mission is revisable throughupdate_study_plan.studyloop doctorreports a plan document it cannot read by name instead of hiding it behind "all ready".
- 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|falsein 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 underdocs/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). Theretrieval_statuspayload'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; explicittorch/onnxalways 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.
- 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/optionscannot 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_progressrow with nolast_seenno longer crashes the struggle collector — and with itstudyloop now— on the way to a recommendation. - The release gate (
scripts/check-release-consistency.py --release) read a foldeddeferred: >-reason in an openspec change's.openspec.yamlas 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'sx-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 anode --testreplay of that race.
- 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 titledSQL $(touch pwned) Windowsused to run its substitution when the offered line was pasted into a shell. anyio4.12.1 → 4.15.1 for two published advisories (CVE-2026-63374, CVE-2026-64847); both dependency audits are clean again.
- 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).
-
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 throughstudyloop study --mode plan-architect(or thestudyloop plan architectalias) to Codex, pi and Grok Build, and installed as a native named agent for Claude Code, Kiro CLI and OpenCode bystudyloop 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 bystudyloop install agents/doctor --fix: OpenCode globally in~/.config/opencode/opencode.jsonand per session, Grok Build throughgrok mcp add(status read fromgrok mcp list --json, the legacyuser-settings.jsonmap honoured read-only). Server names are the same in every harness config;studyloop-mcpandsession-db-mcpare 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-checkcan run before the release tag exists: the release consistency check gained a--pre-tagmode that defers only the tag/date assertion,just release-verifyruns the strict form after tagging, and a release note that is still theprepare-releaseskeleton now fails the gate. -
Semantic session search. Schema 48 adds an embedding substrate to the session database (
message_embeddings, chunked and content-hashed, with asqlite-vecsidecar index);session-maint embedfills the backlog,session-maint embed-check [--fix]audits alignment,studyloop doctorreportsembeddings_alignment, and the export hook keeps vectors current. Every agent-facing search (session_searchover MCP,session-query, the retrieval half ofmemory_search) now goes through oneretrieval.searchservice, 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 withsemantic_search.hybrid: trueor, for one process,STUDYLOOP_RETRIEVAL_MODE=hybrid. -
Doctor checks
exporter_schema(the installedsession-exportagrees with the database schema) andexport_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) drivesuv sync/uv run;studyloop install toolspasses the same minor to everyuv tool install;./scripts/install.shprints 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.shis the documented override. The nightly install workflow now runs the real installer (a separateinstallerjob) and checks Python 3.14 as a signal; CI asserts each matrix job runs the Python it names. -
obsidian.filename_templateis honoured by the Obsidian export (it was documented but never read). -
scripts/check-release-consistency.py --releasealso 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 --fixwrite~/.grok/hooks/studyloop.json(underGROK_HOMEwhen set), aSessionEndhook runningsession-export --grok-onlythat skips a subagent's own teardown and never fails the hook, plus the session-memory mandate in~/.grok/rules/session-db.md. Doctor reportssession_export_hook_grok; the sharedAGENTS.mdwind-down step now names the Grok flag beside Codex's.
-
Export hooks and doctor's
exporter_schemacheck follow uv's real tool bin directory (UV_TOOL_BIN_DIR, elseuv tool dir --bin) instead of assuming~/.local/bin; with the default directory the portable$HOME/.local/binform is unchanged. Found by the first run of theuv tool installsmoke insidejust release-check. The smoke itself now pins the interpreter from.python-versionlike the installer. -
Installing from source no longer depends on whichever
python3happens to be onPATH: the installer used to validate that interpreter and then letuvpick a different one, anduv tool installresolved without any--pythonat all (reported by a user who neededUV_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 --helplists the Grok source, the mentor install guide detectsgrok, 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: nookf/ontologcode 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-checkcommands. Thesemantic_searchexample no longer listsfts_weight/semantic_weight(removed from the defaults as well: fusion is unweighted), thesession_searchMCP docstring names both retrieval modes, and the four copies of an obsolete "schema-30" warning are gone. -
Configuration keys behave as documented.
agent-session-toolshonours a top-levelsession_db:with the same precedence asstudyloop, so both packages open the same database;studyloop doctorno longer flags thedatabase,hosts,semantic_search,thresholds,loggingandexcluded_dirssections the setup guide recommends as unknown;studyloop setupputs the chosen harness first inagents.priorityinstead of writing a deadai_assistantkey; the Obsidian doctor check verifies the memory directory is writable; the undocumentedtts.pausekey 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-progresscommand is replaced by the realtutor-checkpoint <skill> --notes; the Claude Code status line renders the energy label; pi and OpenCode prompts fall back tosession-querywhere the session-db MCP server is not registered; the MCP README says the installer writes Codex's config; the orphaned, wrong-schemaagents/opencode/mcp.jsonis deleted; the installer's next steps no longer name the unshippedstudy-plan-architectagent. -
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-strugglesexample passes the required--harness, the CLI reference documentssession-maint embed/embed-checkand the doctor exit codes accurately, and the study-plans guide acknowledges therecord_plan_learningMCP tool. -
Developer tooling: the Claude Code
openspec-gatehook resolves its script through${CLAUDE_PROJECT_DIR}(acdin the session used to break every shell call), the localci-standardsbandit skip list matches CI, andjust release-checkruns theuv tool installsmoke script. -
studyloop doctorno longer crashes on a Kiro CLI that has migrated thestudy-mentoragent 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: unclassifiedexplicitly for a brand-newconfig.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 — thestudyloopCLI (now exits2), all seven previously-unguarded MCP tool call sites, andsession-db-mcp'sopen_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.updatedon 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.
- 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-targetroute 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 newnoopener,noreferrertab, Obsidian hands the current tab to theobsidian://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, exactlyxtiles.apporapp.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.mdwhen 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 synced0600temporary sibling, and an atomic replace that preserves the destination's mode.brain enableand 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 ofjust release-check) builds the wheel, installsstudyloop[web]into an isolated environment outside the checkout, starts the installed application, requests the launch-target route, and loads every launcher asset reachable frommain.js's import graph — so a packaging regression in the launcher cannot ship invisibly again. - A session-memory reliability candidate:
session-repairinspects existing imports and offers explicit, backed-up repair;session-sync allreconciles 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 agentsinstalls onestudyloop-session-memoryskill (MCP-first query withsession-queryfallback) 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 --fixuses 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.
- 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 launchesstudyloop-mcp --dev, orstudyloop web --devfor the HTTP API. Removing the flag removes the surface entirely (unknown CLI command, noexercise_*tools intools/list, no/api/exercisesroutes) 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.
- 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-pinnedhttpx2==2.12.0successor package; and the real PTY transport tests scope Python 3.13's multithreadedforkpty()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.
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).
- 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_cardsreturns 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 agentsno 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.
- 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-runnow 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) andstudyloop plan evaluateno longer fail when searching session history: the full-text query joined two tables that both carry acontentcolumn without qualifying it, and OR'd multipleMATCHconstraints, which FTS5 refuses. Multi-word topics such as "window functions" now also match as phrases rather than scattered terms.
- An optional second-brain layer:
studyloop brain statusreports whether a provider is configured, andstudyloop brain publish --plan <id>publishes a projection of that plan. Nothing is written anywhere until you opt in with asecond_brain:section inconfig.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 brainpublishes plans and today's study to a configured second brain, pulls your notes back on request, andstudyloop config initcan enable it when you point StudyLoop at a vault.studyloop doctorreports 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 agentsinstalls an opt-in wind-down skill into every harness it detects. The skill is silent unless your provider isxtilesand anxtilesserver is connected, and StudyLoop stores no xTiles credential.
- 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.
- 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,templateanddaily_noteare gone. If any of them is still in yourconfig.yaml, StudyLoop now says so and stops rather than ignoring it —daily_noteused to append a line to your own daily note, and you should know that it no longer does.
- 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.xonly, pre-1.0), and a two-sentence summary of the LAN-password and encrypted-secrets-store threat model.THIRD-PARTY-NOTICES.mdnow credits all 26 vendored web assets underweb/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.
- 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-commandnow requires--yes(or an interactiveyat the prompt) in addition to--run-commandbefore 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,<2everywhere it is declared, so an unlocked install (a futurepip install/uv tool installoutside this repo's lockfile) can no longer select the 2.x release that renamedFastMCPand breaksstudyloop-mcp/session-db-mcpat 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.tomlversion now matchespackages/studyloop/pyproject.toml's (both0.1.0); the release-consistency check now fails if the two ever disagree again. - Removed the
studyloop[sessions]extra and droppedsessionsfromstudyloop[all]:agent-session-toolsis 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.shalready add it unconditionally, independent of any extra.studyloop[all]still expands tocontent,bedrock,notebooklm,tui,web, andmcp; every one of those (plusallitself) is now proven, per-extra, to install and import from a bare built wheel with no workspace present.
- 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.
ttydentirely — the browser terminal fallback and the server transport that backed it.studyloop study --transport,studyloop web --ttyd-port, thettyd_portconfig key, the/terminal/HTTP+WebSocket proxy, and thetransport: "ttyd"/STUDYLOOP_TRANSPORT=ttydrequest paths are all removed; requestingttydnow fails with a clear error instead of being silently downgraded to another transport.studyloop studyis unaffected — it never depended on ttyd to work. See ADR-0008.
THIRD-PARTY-NOTICES.mdlinked 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 upstreamNOTICEfile at its pinned version.THIRD-PARTY-NOTICES.mdsaidvendor/"ships 26 third-party files"; four of those 26 are first-party@font-faceglue 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/endrequest 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.mdsaid "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.mdis now "Web UI" and states the app is installable but does not work offline, matching the qualifier already used ondocs/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 globalstudyloop install agents --tool opencodewrite to~/.config/opencode/agents/, and the separate project-local.opencode/agents/study-mentor.md+.opencode/opencode.jsonthatstudyloop study --agent opencodewrites at session start. Split into two clearly-labelled paragraphs.releases/v0.1.0.mdhardcoded 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.mdnow documents the 8contentsubcommands that manage NotebookLM notebooks/podcast syllabi (autopilot,delete,download,generate,list,status,syllabus) and the fast content index (index), plusstudyloop topic(singular) — the command AI agents run every session to log activity, which had no public reference entry at all.docs/content-pipeline.mdgained a short table for the NotebookLM subcommands and a paragraph oncontent index.studyloop doctor --category updateswas an advertised,--help-listed choice that always produced zero results (nothing is published, so its one check is deliberately unregistered) and an unreachable--fixbranch. 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 undercore.- The error
studyloop install tools/agentsanddoctor --fixraise outside a source checkout named a pre-studyctl-rename project ("socratic-study-mentor") instead of studyloop. studyloop doctornow checks for tmux (3.1+,studyloop study's hard dependency) under thecorecategory, with an install hint, instead of giving no signal at all before a firststudyloop studyfails 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 shownow also warns about unknown or retired top-level config keys (a misspelled key, or one left over from a removed feature) —doctoralready 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 matchlogic/break_logic.py.studyloop backlog add/list/resolve --helptold you to runstudyloop topics ...— a different real command that rejects those arguments. The examples now saystudyloop backlog ..., matchingsuggest's docstring, which was already correct.- The TUI sidebar's
+5min focuskey 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 7dondocs/setup-guide.mdanddocs/cli-reference.mdraised an unhandled traceback if pasted verbatim —query_utils.parse_dateonly acceptsYYYY-MM-DDorlast-N-days. Both pages now show--since last-7-days, matching the fix already applied todocs/first-week.md.docs/setup-guide.md's "Interactive Setup" section describedstudyloop config init's three questions (knowledge bridging, a "study material location" questionsetupnever asks, an Obsidian vault path) under thestudyloop setupheading. It now describessetup's real, shorter flow (notes folder, optional focus-topic confirmation, optional harness pick) and correctly separates outconfig init's own three questions instead of conflating the two commands.- Removed the dangling
GEMINI.mdsymlink at the repo root (it pointed atagents/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 onupdated_at, matching the check already applied tosessions/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.pushand the remote side ofsyncnow back up the destination before writing to it, exactly aspullalready backs up its own destination. - Session archive/delete/prune cutoffs (
session-maint archive/delete-old, tiering'sprune/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 tostudy_progressnow runs inside an explicitdb.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 throughrecord_teachback; and thestudy_progressrow 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}.pyandlearning/mastery.pycaught everysqlite3.OperationalErrorthe 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) usedwith 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 asqlite3.Connection, relying on CPython's refcounting to eventually close the file handle. Nowtry/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/studyloopfor session-state IPC files and spawned the CLI without settingSTUDYLOOP_SESSION_DIR. Running these suites locally read, wrote, and deleted the developer's real live session state. Every path now derives fromSTUDYLOOP_SESSION_DIR, redirected to atmp_path-based directory by each fixture, and forwarded to every spawnedstudyloopsubprocess. - Cross-machine sync's recency gate (
excluded.updated_at > table.updated_at) was NULL-falsy: a destination row with a NULLupdated_atcould never be overwritten by any incoming row, however new, freezing it forever. Everystudy_sessionsrow created after the R-19 migration was silently one of these, since that table'supdated_athad no default and nothing wrote it. NowCOALESCE(destination.updated_at, '')treats a NULL destination as older than anything, and a trigger stampsupdated_aton every insert and update to the tables that previously had no default for it. - Cross-machine sync used
id INTEGER PRIMARY KEY AUTOINCREMENTas the conflict target forteach_back_scores,knowledge_bridges,parked_topics, andscrub_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-backfilledsync_key;concept_relations(which already had a real natural key) now syncs on that instead of its own autoincrementid. - Cross-machine sync's remote backup (
push/the remote side ofsync) and the localsession-maintbackup helper both copied the database file with a plain file copy (cp -pover SSH,shutil.copy2locally). In WAL mode, data committed but not yet checkpointed into the main.dbfile lives in the sibling-walfile, 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.backupdot-command remotely,sqlite3.Connection.backup()locally). pushand the remote-writing step ofsyncdiscarded 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: everypush/synccall 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_atas 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, aT-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'sdatetime(), so it stays correct if a future writer, a manual edit, or an older/foreign row ever disagrees with today's uniform writers. record_teachbackcaught everysqlite3.DatabaseErrorthe same way, including a genuine lock/timeoutOperationalError-- indistinguishable from an expected CHECK-constraint rejection (an out-of-range teach-back score), both silently returning "not recorded." Narrowed tosqlite3.IntegrityErrorfor 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 acrosshistory/*.py.history/concepts.py'slist_conceptsandhistory/search.py'stopic_frequency/struggle_topicshad the same bareexcept sqlite3.OperationalError: return []the rest ofhistory/*.pywas 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 studyno 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 --allis the one place left to deliberately sweep everystudy-*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.
- The Claude adapter's launch command (executed via
/bin/sh -c) is now built withshlex.quote()around both the binary path and the persona file path. Neither is attacker-controlled in the normal flow, but the persona path comes fromtempfile.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
.envfile planted in or above the directorystudyloopis run from could set the test-onlySTUDYLOOP_TEST_AGENT_CMD/STUDYLOOP_TEST_ACP_CMDhatch 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.envauto-loader is deleted and logged. - Closed a second, independent gap that bypassed the fix above: a
.envat~/.config/studyloop/.envreintroduced the same test hatch at every web-server startup, via a second dotenv loader inagent-session-toolsthat 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
.envfile is loaded, rather than re-reading the environment on every session start. A.envloaded by something other than this package, later in the process's life, can no longer set the hatch either. practice verify --run-commandnow 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
_KEYsuffix (ENCRYPTION_KEY,SIGNING_KEY,MASTER_KEY, ...) and bareAUTHORIZATION/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 holdlan_passwordin 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.jsonnow 404s, matching the already-disabled/docsand/redoc. practice verify --run-commandno 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.
- 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.