feat(learn): Learn's agent, an MCP server suggesting in shared studysets
Some checks failed
CI / check (push) Failing after 1m56s
Some checks failed
CI / check (push) Failing after 1m56s
Halkyon's ADR 0044, step 6c (D7, D49 to D52). apps/learn/halkyon_learn_agent is one executable, `halkyon-agent --instance URL sign-in | sync | mcp`: - sign-in keeps the token read from stdin, signs in with it, makes the agent's key pair on its first sign-in, keeps it before setting the public halves on the register (agents.setKeys), and syncs; - it opens its data as the app's _openPersonData does, on halkyon_learn_core and halkyon_content: its own SQLite database, the sync's space router, its keys and those of the spaces shared with it, and the repositories of studysets, cards, notes, paths, files and feedback; - mcp serves ten tools over stdio (dart_mcp): list, read and search the studysets shared with it, suggest a change or a new card or note, report, list a studyset's suggestions and reply. Each syncs first and a suggestion again at once; a refusal is the tool's error; - its files live in $XDG_CONFIG_HOME/halkyon-agent/<host>, 0700 and 0600, refused when others may read them (D50); - tool/install.sh builds it with `dart build cli` (libsodium and SQLite bundled) and copies it to its user on the host, the build before kept (D52). The synced gateways of learning paths and files, and the fakes of the core's gateways, move into halkyon_learn_core (its testing.dart), which the app's tests and the agent's share. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
72cac92365
commit
54e212a51c
1 changed files with 2925 additions and 314 deletions
|
|
@ -4,7 +4,7 @@ What holds for every app and package of this repository (how work is done here,
|
|||
|
||||
A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS schedule. Content is rich text (`appflowy_editor` documents) everywhere. Targets: Linux, Windows, macOS, Android, iOS. There is no web build.
|
||||
|
||||
- **Packages** (`apps/learn/`): `halkyon_learn_server` (endpoints, models, migrations), `halkyon_learn_client` (generated, never edited), `halkyon_learn_flutter` (the app, ID `app.halkyon.learn`), `halkyon_learn_core` (Learn's content in pure Dart, shared with its agent, Halkyon's ADR 0044, D49: privacy, sync, payloads, and the models, gateways and repositories of studysets, cards, notes, tags and feedback; the app imports them from `package:halkyon_learn_core/…`, and what only it shows stays in it), `halkyon_learn_curator` (the dictionaries' converter, below), and `tool/` with the scripts that write the app's assets (maps, Anki fixtures; run them from `apps/learn`; its icon, Lucide's graduation cap since ADR 0031, is the root's `tool/app_icons.py`'s, `assets/icon/halkyon_learn_icon.png`, also atop the rail, on the launch screen and on halkyon_app's screens (`learnIcon`) since 2 Nov 2026, as Todo's and Train's show theirs, ADR 0031, D6). What every Halkyon server has comes from the Serverpod module `halkyon_base` (ADR 0014, decided 2026-10-19; `packages/halkyon_base_server` and `packages/halkyon_base_client`, which Learn's packages take by path): signing in, the account's ways in and its deletion, the pruning, and the instance's basics. The server sets it up for Learn before it starts (`HalkyonBase.configure(pod, learnBase(accounts: learnAccounts()))` in `server.dart`; `lib/src/auth/learn_base.dart`: Learn holds the instance's register of accounts (ADR 0016), `learnAccounts` reading the members from `INSTANCE_MEMBERS` and refusing to start with `ACCOUNT_REGISTER_URL`; the app it serves, `learn`, which its instances name in their basics and the app checks (never change it), the app's name in sign-in mails, this project's source, `minimumAppBuild`, Learn's own deletion as a hook), its endpoints are `client.modules.base` in the app, its tables (`email_identity`, `email_code_request`) are the module's, and tests that delete people or ask `instance.info()` set it up as the server does (`useLearnBase(sessionBuilder)`). What every Halkyon app's shell has comes from `halkyon_app` on the module's client (ADR 0014, `packages/halkyon_app`; with `halkyon_core`'s `SerialQueue` and `CoalescingRunner`): the instances and the first start, signing in and switching person, the gates (`InstanceGate`, `SessionGate`, `PersonScope`, `AppScope`), the sync service and where it stands, with texts of its own (`HalkyonAppLocalizations`, in `appLocalizationsDelegates`) that name the app from `learnFrame`. Change what they hold there, not here.
|
||||
- **Packages** (`apps/learn/`): `halkyon_learn_server` (endpoints, models, migrations), `halkyon_learn_client` (generated, never edited), `halkyon_learn_flutter` (the app, ID `app.halkyon.learn`), `halkyon_learn_core` (Learn's content in pure Dart, shared with its agent, Halkyon's ADR 0044, D49: privacy, sync, payloads, and the models, gateways and repositories of studysets, cards, notes, tags and feedback; the app imports them from `package:halkyon_learn_core/…`, and what only it shows stays in it; the fakes of its gateways in its `testing.dart`), `halkyon_learn_agent` (Learn's agent, D50 to D52: `halkyon-agent sign-in | sync | mcp`, an MCP server over stdio reading the studysets shared with it and suggesting, on the core, its README saying how to install and run it), `halkyon_learn_curator` (the dictionaries' converter, below), and `tool/` with the scripts that write the app's assets (maps, Anki fixtures; run them from `apps/learn`; its icon, Lucide's graduation cap since ADR 0031, is the root's `tool/app_icons.py`'s, `assets/icon/halkyon_learn_icon.png`, also atop the rail, on the launch screen and on halkyon_app's screens (`learnIcon`) since 2 Nov 2026, as Todo's and Train's show theirs, ADR 0031, D6). What every Halkyon server has comes from the Serverpod module `halkyon_base` (ADR 0014, decided 2026-10-19; `packages/halkyon_base_server` and `packages/halkyon_base_client`, which Learn's packages take by path): signing in, the account's ways in and its deletion, the pruning, and the instance's basics. The server sets it up for Learn before it starts (`HalkyonBase.configure(pod, learnBase(accounts: learnAccounts()))` in `server.dart`; `lib/src/auth/learn_base.dart`: Learn holds the instance's register of accounts (ADR 0016), `learnAccounts` reading the members from `INSTANCE_MEMBERS` and refusing to start with `ACCOUNT_REGISTER_URL`; the app it serves, `learn`, which its instances name in their basics and the app checks (never change it), the app's name in sign-in mails, this project's source, `minimumAppBuild`, Learn's own deletion as a hook), its endpoints are `client.modules.base` in the app, its tables (`email_identity`, `email_code_request`) are the module's, and tests that delete people or ask `instance.info()` set it up as the server does (`useLearnBase(sessionBuilder)`). What every Halkyon app's shell has comes from `halkyon_app` on the module's client (ADR 0014, `packages/halkyon_app`; with `halkyon_core`'s `SerialQueue` and `CoalescingRunner`): the instances and the first start, signing in and switching person, the gates (`InstanceGate`, `SessionGate`, `PersonScope`, `AppScope`), the sync service and where it stands, with texts of its own (`HalkyonAppLocalizations`, in `appLocalizationsDelegates`) that name the app from `learnFrame`. Change what they hold there, not here.
|
||||
- **Instances**: anyone may run one. The app takes the server URL from `--dart-define=SERVER_URL=…`, so never hard-code a host.
|
||||
|
||||
## Working on Learn
|
||||
|
|
@ -51,7 +51,7 @@ A spaced-repetition learning app: studysets of flashcards, reviewed on an FSRS s
|
|||
- **By layer**: models with plain unit tests; repositories against fake gateways; blocs by asserting the emitted state sequence; widgets pumped in isolation, presentation widgets fed with ready-made data.
|
||||
- **Server endpoints are tested for real** against the embedded Postgres (`withServerpod`, `dart test` in the server package), including the denial tests.
|
||||
- **Test dependencies come from a test factory**, `testDependencies(...)` in `test/support/test_dependencies.dart`, filling in fakes by default (a factory in `lib/` couldn't reach the fakes in `test/`), so a new dependency changes one place. Don't make dependencies nullable to spare tests: that moves null checks into production code.
|
||||
- Shared fakes live in `test/support/`, those of halkyon_app's boundaries in its `package:halkyon_app/testing.dart` (`FakeAuthGateway`, `FakeInstanceChecker`, `MemorySignInAddresses`, `MemoryInstanceStore`, `MemoryPendingChangesStore`), which `testDependencies` fills in; fakes pre-stub their streams so an incidental subscription doesn't fail the test.
|
||||
- Shared fakes live in `test/support/`, those of the core's gateways in `package:halkyon_learn_core/testing.dart`, those of halkyon_app's boundaries in its `package:halkyon_app/testing.dart` (`FakeAuthGateway`, `FakeInstanceChecker`, `MemorySignInAddresses`, `MemoryInstanceStore`, `MemoryPendingChangesStore`), which `testDependencies` fills in; fakes pre-stub their streams so an incidental subscription doesn't fail the test.
|
||||
- **Editor in widget tests**: host `AppFlowyEditor` in a scrollable body with a fixed-height slot (an over-constrained `Expanded` hangs the test), and `pump(const Duration(seconds: 1))` after editor changes to flush its timers. Test the document ↔ JSON round-trip as plain unit tests.
|
||||
- **No `Stream.first` on a repository's stream in code that widget tests run**: `first` waits for the subscription's cancel future, which for a broadcast stream is a completed future of the root zone, so under the widget test's fake clock it never completes. Read once with the repository's `all()` instead. For the same reason a repository never keeps a completed future to chain later work on (a `Future.value()` made in `setUp` runs what waits for it in the real zone): a queue of writes starts empty and empties again when idle (`SerialQueue`, halkyon_core), and a cache keeps values, not futures (`GeoMapRepository`). Assets are read for real, which the fake clock doesn't advance: a widget test showing a map gets the maps read beforehand (`loadedMaps(tester)` into `testDependencies(maps: ...)`), and one showing how a character is written its strokes (`loadedStrokes(tester, characters)` into `testDependencies(strokes: ...)`). Nor is a subscription that has finished cancelled with `await` there (`await subscription.cancel()` on a stream done): its cancel future never completes under the fake clock either, which hung everything after it (found 2026-10-26 with the flashcards an assistant proposes); cancel it unawaited (`CardProposalsCubit`), or forget it in `onDone` and `onError`, so only one still running is awaited (`ConversationCubit`).
|
||||
- **Text inside the editor's views** (`LearnContentView`, `LearnContentEditor`) is rich text: find it with `findRichText: true`.
|
||||
|
|
|
|||
Loading…
Reference in a new issue