Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
2732ccc
fix(tests): grace-release test polls the settled lifecycle, not the f…
NetDevAutomate Sep 4, 2026
0514f93
fix(tests): accessible-name smoke check matches button names exactly
NetDevAutomate Sep 4, 2026
b961b72
fix(e2e): async wait_for_function predicates never waited — poll from…
NetDevAutomate Sep 4, 2026
3148bf8
fix(history): topic_frequency's FTS query was unrunnable — qualify an…
NetDevAutomate Sep 4, 2026
7639999
feat(brain): the wind-down decision becomes data — studyloop brain wi…
NetDevAutomate Sep 4, 2026
ec3e063
docs(second-brain): say what was actually proven — the 0.2.1 wording …
NetDevAutomate Sep 4, 2026
c5cd56b
feat(plan): a learning-record writer — studyloop plan record + record…
NetDevAutomate Sep 4, 2026
ff1bed9
fix(obsidian): close review residuals O1, O4, O7 in the vault writer
NetDevAutomate Sep 4, 2026
55fe85c
test(harness): a stub xtiles MCP server, and WD-4 to pin it (Layer 2)
NetDevAutomate Sep 4, 2026
6430580
chore(openspec): reconcile and archive the second-brain change; guard…
NetDevAutomate Sep 4, 2026
b27788d
chore(release): wire release-consistency-shipped into release-check; …
NetDevAutomate Sep 4, 2026
a5ddea2
fix(tests): narrow the stub tile response to TextContent before readi…
NetDevAutomate Sep 4, 2026
ced3ad4
test(plan): satisfy the CLI coverage gate for plan record
NetDevAutomate Sep 4, 2026
70d7f1e
test(harness): WD-5/WD-6 live transcript capture, and the four defect…
NetDevAutomate Sep 4, 2026
064bae9
fix(protocol): a declined offer is acknowledged by moving on, never b…
NetDevAutomate Sep 4, 2026
40664ef
feat(hooks): the openspec early-warning hook — one body, three harnes…
NetDevAutomate Sep 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "uv run python scripts/openspec-gate.py pre-tool-use",
"timeout": 30
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "uv run python scripts/openspec-gate.py remind",
"timeout": 30
}
]
}
]
}
}
26 changes: 26 additions & 0 deletions .codex/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"type": "command",
"command": "uv run python scripts/openspec-gate.py pre-tool-use",
"timeout": 30
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "uv run python scripts/openspec-gate.py remind",
"timeout": 30
}
]
}
]
}
}
22 changes: 22 additions & 0 deletions .kiro/hooks/openspec-gate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"version": "v1",
"hooks": [
{
"name": "openspec-gate (early warning, not enforcement)",
"trigger": "PreToolUse",
"matcher": "execute_bash",
"action": {
"type": "command",
"command": "uv run python scripts/openspec-gate.py pre-tool-use"
}
},
{
"name": "openspec-remind",
"trigger": "UserPromptSubmit",
"action": {
"type": "command",
"command": "uv run python scripts/openspec-gate.py remind"
}
}
]
}
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,60 @@ experience may change before `1.0.0`.

## [Unreleased]

### 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 record is not written back into the
plan document, and that planner tiles can be removed through the connector
while pages and projects must be deleted in the xTiles interface by hand.
Comment on lines +39 to +44
- `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
Expand Down
24 changes: 23 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,31 @@ smoke-extras:
build-release:
./scripts/build-release.sh

# WD-5/WD-6: the live wind-down gate checks, captured through Claude Code
# headless against the LiteLLM gateway (no vendor credential; the key is read
# from the proxy's own config at runtime). Opt-in — burns gateway spend
# (estimate: reviews/2026-09-04-gate-checks/ESTIMATE.md). Writes transcripts
# and the pass/fail summary under reviews/…/evidence/gate-checks/.
gate-checks:
STUDYLOOP_EVIDENCE_DIR={{justfile_directory()}}/reviews/2026-09-04-gate-checks/evidence/gate-checks \
uv run --group dev pytest packages/studyloop/tests/live/test_wind_down_transcripts.py -m live_provider -q

release-consistency:
uv run python scripts/check-release-consistency.py --skip-wheel

# The release-mode superset: everything above PLUS the openspec guards — a
# change with commits since the last tag must be archived or carry a
# `deferred: <reason>`, and archive entries ADDED since the last tag must pass
# `openspec validate` (soft-skipped when the CLI is absent, same convention as
# spec-check; not `--archived --all`, because a July archive predating this
# guard has unticked tasks nobody has evidence to reconcile, and re-failing
# every future release on it would teach people to ignore the gate).
# Deliberately NOT part of preflight: open changes are legal during a cycle;
# only shipping one is not. Both guards would have fired on the 0.2.0 cut
# (2026-09-04 review, Q5).
release-consistency-shipped:
uv run python scripts/check-release-consistency.py --skip-wheel --release

prepare-release version:
uv run python scripts/prepare-release.py {{version}}

Expand Down Expand Up @@ -193,7 +215,7 @@ xtiles-auth:

preflight: lint typecheck test test-js docs release-consistency spec-check

release-check: test test-js lint typecheck shellcheck docs audit audit-full release-consistency smoke-installed smoke-extras
release-check: test test-js lint typecheck shellcheck docs audit audit-full release-consistency-shipped smoke-installed smoke-extras

# "Would GitHub Actions pass?" locally, before pushing. `check` runs the
# host-answerable gates (lint, typecheck, test, sast, audit, docs, ...); `lint`
Expand Down
36 changes: 18 additions & 18 deletions agents/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,63 @@
"agents": {
"claude/socratic-mentor.md": {
"hash": "42880ffa80484ec9",
"updated": "2026-09-03"
"updated": "2026-09-04"
},
"codex/AGENTS.md": {
"hash": "23dc3d4fbdea89c3",
"updated": "2026-09-03"
"updated": "2026-09-04"
},
"kiro/study-mentor.json": {
"hash": "c9f2302ebc8d3c39",
"updated": "2026-09-03"
"updated": "2026-09-04"
},
"opencode/study-mentor.md": {
"hash": "0ff72c058de9f7d2",
"updated": "2026-09-03"
"updated": "2026-09-04"
},
"pi/AGENTS.md": {
"hash": "8706855f619c71b5",
"updated": "2026-09-03"
"updated": "2026-09-04"
},
"shared/audhd-framework.md": {
"hash": "8b694064b100741f",
"updated": "2026-09-03"
"updated": "2026-09-04"
},
"shared/break-science.md": {
"hash": "74541a44431f7f6a",
"updated": "2026-09-03"
"updated": "2026-09-04"
},
"shared/knowledge-bridging.md": {
"hash": "adb4221cda83498f",
"updated": "2026-09-03"
"updated": "2026-09-04"
},
"shared/network-bridges.md": {
"hash": "8af4732b77cc15ae",
"updated": "2026-09-03"
"updated": "2026-09-04"
},
"shared/session-protocol.md": {
"hash": "b58178df20cb9008",
"updated": "2026-09-03"
"updated": "2026-09-04"
},
"shared/socratic-engine.md": {
"hash": "42738fba9479ec14",
"updated": "2026-09-03"
"updated": "2026-09-04"
},
"shared/teach-back-protocol.md": {
"hash": "9bbe8831f1c74837",
"updated": "2026-09-03"
"updated": "2026-09-04"
},
"shared/wind-down-protocol.md": {
"hash": "d9d0dde390e03b4c",
"updated": "2026-09-03"
"hash": "5b1ec3303b8d1086",
"updated": "2026-09-04"
},
"skills/studyloop-xtiles-wind-down/SKILL.md": {
"hash": "aa31c59092ffe929",
"updated": "2026-09-03"
"hash": "a2e84febb865f24d",
"updated": "2026-09-04"
},
"skills/studyloop-xtiles-wind-down/references/harnesses.md": {
"hash": "e3f0ebc00ee792a8",
"updated": "2026-09-03"
"hash": "d2beeed44e1c0154",
"updated": "2026-09-04"
}
}
}
47 changes: 36 additions & 11 deletions agents/shared/wind-down-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,29 @@ Standard end-of-session tasks from session-protocol.md:

4. Set next review dates via spaced repetition schedule.

5. Offer the second brain — **only if one is configured, and only once**:
5. Offer the second brain — **only if the decision command says to, and only
once**. Run this step silently: never announce, name or narrate it in any
state — the learner hears either the offer sentence or nothing at all.

```bash
studyloop brain status --json
studyloop brain wind-down --json --connector <name>
```

If **both** `configured` and `supports_publish` are `true`, offer exactly once,
in one sentence:
```bash
studyloop brain wind-down --json
```

That is the whole command when no MCP tools are visible to you. Add
`--connector <server>` for each MCP server whose tools you can actually
see in this session — MCP tools are named `mcp__<server>__…`, so the
server name is read off the tools themselves, and only `xtiles` affects
the decision. The flag is a statement of fact about this session, not part
of the command's syntax: **naming a connector whose tools you cannot see
fabricates an offer for a service the session cannot reach.** The command
answers with `channel`, `offer`, `sentence` and `reason`.

If `offer` is `true`, say `sentence` **verbatim, exactly once**. For the
`publish` channel that sentence is:

<!-- wind-down-offer -->
Want me to publish today's study record and this plan to your Obsidian vault (Study/Today.md and Study/Plans/<plan-id>.md)? Yes or no — I'll only ask once.
Expand All @@ -67,13 +82,23 @@ Standard end-of-session tasks from session-protocol.md:
studyloop brain publish --today --plan <plan-id>
```

On **no**, or in any other case, **say nothing about second brains at all** and
continue the wind-down. Do not repeat the offer later in the session.

Both flags are required, not just `configured`. A learner on xTiles *is*
configured but has no programmatic backend (`supports_publish: false`), so
offering the publish command would name something that cannot work — and would
do it at the end of every session.
For the `xtiles` channel, follow the `studyloop-xtiles-wind-down` skill,
which carries its own pinned sentence.

If `offer` is `false`, or on **no**, **say nothing about second brains at
all** and continue the wind-down. Do not repeat the offer later in the
session. "No second brain is configured, so nothing to offer", "running
the second brain check now", and "the learner declined, so no xTiles
write happens" are all violations, not courtesies — a declined offer is
acknowledged by moving on, never by naming what was declined.

The command computes two separate rules — not one conjunction. `configured`
plus `supports_publish` (both from `studyloop brain status --json`) selects
the publish offer; provider `xtiles` plus a connected `xtiles` connector
selects the skill's offer. A learner on xTiles *is* configured but has no
programmatic backend (`supports_publish: false`), so the publish sentence
would name something that cannot work — which is why the decision lives in
the command rather than in this prose.

### Phase 2: Consolidation Guidance (spoken if voice mode is active)

Expand Down
40 changes: 29 additions & 11 deletions agents/skills/studyloop-xtiles-wind-down/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,53 @@ you, not a feature of the CLI.
Use only during Phase 1 of `~/.agents/shared/wind-down-protocol.md`, after progress
has been recorded with `studyloop progress "<concept>" -t <topic> -c <confidence>`.

## Gate — both halves, checked every session
## Gate — decided by the CLI, checked every session

Run the decision command, naming each MCP server connected in this session:

```bash
studyloop brain wind-down --json --connector xtiles
```

Offer only when it answers `"channel": "xtiles"` with `"offer": true`. That
happens exactly when both halves of the gate hold:

1. `studyloop brain status --json` reports `provider: xtiles`. Any other provider
means this file does not apply: an Obsidian learner has already been offered the
publish command in Phase 1, and a learner on `none` has chosen neither.
2. An MCP server named `xtiles` is connected in this session — its tools are
visible to you. If it is not, do nothing and say nothing about xTiles. Do not
suggest they connect one; an offer to set up a service they never asked for is
the thing this gate exists to prevent.
visible to you, and you passed `--connector xtiles` to say so. If it is not,
do nothing and say nothing about xTiles. Do not suggest they connect one; an
offer to set up a service they never asked for is the thing this gate exists
to prevent.

If either half is false, continue the wind-down without mentioning xTiles at all.
If `offer` is false, continue the wind-down without mentioning xTiles at all.

## The offer

Offer once, in one line:
Offer once, in one line — the `sentence` the command returned, verbatim:

<!-- xtiles-wind-down-offer -->
> Want me to add today's learning record and the next review to your xTiles project? Yes or no — I'll only ask once.
<!-- /xtiles-wind-down-offer -->

On **yes**, follow prompt P3 from the Second Brain guide: one learning-record page
under the plan's project, one dated planner task for the next review, then say what
you wrote. Ask before each write, and if xTiles refuses a write, report what it
said rather than retrying.
On **yes**, record into the plan **first**: `studyloop plan record <plan-id>
--title "<topic>" --body "<the summary>"` (or the `record_plan_learning` MCP
tool). The plan document is the source of truth, and the xTiles page must be a
projection of a record it already has — never the only copy. Then follow prompt
P3 from the Second Brain guide: one learning-record page under the plan's
project, one dated planner task for the next review, then say what you wrote.
Ask before each write, and if xTiles refuses a write, report what it said
rather than retrying.

On **no**, continue the wind-down and do not raise it again this session.

## What this sends, and where

The summary, the plan title and the next review date go to the model service
backing this session and, through the connector, to xTiles' cloud. xTiles asks
backing this session and, through the connector, to xTiles' cloud. The due
reviews come from the learner's whole study state, not one plan's — nothing in
these tools is plan-scoped. xTiles asks
permission per request, and your assistant reaches only what the learner's own
xTiles account can already see — nothing is shared with other users. StudyLoop
stores no xTiles credential and keeps no copy of what was written: the sign-in
Expand Down
Loading
Loading