From 81958f78e9e5e4b837634b9807ee54f12b3c7b01 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Mon, 10 Aug 2026 20:57:27 +0300 Subject: [PATCH 1/3] fix(handoff): close what stops a Hub agent working without the LearnStack tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Preparation for handing this repository to its own agents. Findings come from an autonomy audit; each was re-verified against the working tree here before being touched. **Three runtime defects an agent hits on day one.** - Both places that tell you how to start the API give a project path that does not exist. It is `src/Core/LearnStack.Hub.Api`, not `src/LearnStack.Hub.Api`, so `dotnet run` fails immediately. - The API cannot reach its database when started the documented way. The host never loads `.env`, so the connection string falls back to defaults. Both run blocks now export it the way `scripts/seed.sh` already does. - `.env.example` and `infra/compose/dev.yml` provision `HUB_POSTGRES_DB`; `HubFoundationRegistration` read `POSTGRES_DB_HUB`. It worked only because both sides defaulted to the same literal — the first time someone changed the database name, compose and the app would have disagreed silently. **Skills sent the agent to a checkout it is not supposed to need.** Eleven references across nine skills pointed at `../LearnStack/docs/...` or `../LearnStack/backend/src/...` for reading authority or source. Reading LearnStack does not require a clone — it requires a URL, which is the rule this repository already states for Markdown links. All are now absolute GitHub URLs. `../LearnStack` survives only where it is genuinely a shell path: `make dev`, the compose stack, the Keycloak realm files. The sharpest case was `wire-cross-cutting-foundation`, whose Step 1 still said "reproduce `../LearnStack/backend/src/LearnStack.SharedKernel/` folder-for-folder". That kernel shipped with P02c-1 and is on `main`; the skill is now about understanding what is there, which is what the owed SharedKernel reconciliation actually needs. **Skills taught rules the corpus has retired.** `add-integration-event` carried the "four HTTPS endpoints, a fifth needs an ADR" framing in four places and `standards-check` had it as a checklist box — ADR-0034 replaced the count with two invariants precisely because protecting it caused TLS private keys to be tunnelled through the entitlement payload. Two skills also described the link audit backwards, telling the agent to check that sibling-relative links "resolve locally" when CI now **fails** on them. **The freeze was unreachable from the entry point.** `start-task` Step 2 checked packet fit against the roadmap but never mentioned that P02c-2 and everything after it is frozen — so an agent asked to build the internal API would have been green-lit into it. Step 2 now checks the freeze first and stops. **`meta` claimed work it does not do.** Its only step is the Markdown link audit; the job name and CONTRIBUTING both advertised format verification, which runs in `backend`. Also: `HubBoundaryTests`' summary described a corpus drift the restructure already closed — Architecture 24 now spells the test `Hub_NeverStores_TenantData` and lists seven, so the KNOWN CORPUS DRIFT block is deleted rather than updated. `openapi-diff` moves off `if: false`, which actionlint rejects, to an unset repository variable. And the two skills the remaining packets need but nobody has written — `add-recurring-job` and `add-contract-test` — are registered in the deferred table instead of being discovered missing. Co-Authored-By: Claude Opus 5 (1M context) --- .claude/skills/README.md | 8 ++++--- .claude/skills/add-feature-key/SKILL.md | 2 +- .claude/skills/add-integration-event/SKILL.md | 14 ++++++++---- .claude/skills/code-review/SKILL.md | 4 ++-- .claude/skills/commit-and-pr/SKILL.md | 4 ++-- .claude/skills/implement-task/SKILL.md | 9 +++++--- .claude/skills/local-dev-setup/SKILL.md | 9 ++++++-- .claude/skills/standards-check/SKILL.md | 12 ++++++++-- .claude/skills/start-task/SKILL.md | 15 +++++++++++-- .claude/skills/update-glossary/SKILL.md | 4 ++-- .../wire-cross-cutting-foundation/SKILL.md | 15 +++++++++---- .claude/skills/write-adr/SKILL.md | 5 +++-- .github/workflows/ci.yml | 10 ++++++--- CONTRIBUTING.md | 3 ++- README.md | 9 ++++++-- .../Composition/HubFoundationRegistration.cs | 5 ++++- .../HubBoundaryTests.cs | 22 ++++++------------- .../RepositoryLayoutTests.cs | 3 ++- infra/compose/README.md | 2 +- 19 files changed, 102 insertions(+), 53 deletions(-) diff --git a/.claude/skills/README.md b/.claude/skills/README.md index 4dd6bbe..fcfc6b6 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -38,7 +38,7 @@ Pick the entry point matching the user's intent. Only **one** entry point runs p | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | [implement-task](implement-task/SKILL.md) | The default entry point for substantive work — scope, implement, self-check, test, docs, commit, review prompt. | | [start-task](start-task/SKILL.md) | Lightweight scoping-only entry point. Reading order + alignment check. | -| [write-adr](write-adr/SKILL.md) | Capturing a **Hub-internal** decision (`HUB-NNNN` series). Cross-cutting decisions go in `../LearnStack/docs/decisions/`. | +| [write-adr](write-adr/SKILL.md) | Capturing a **Hub-internal** decision (`HUB-NNNN` series). Cross-cutting decisions go in [LearnStack's `docs/decisions/`](https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/). | | [update-glossary](update-glossary/SKILL.md) | Introducing a Hub-specific term in `docs/glossary.md`. | | [commit-and-pr](commit-and-pr/SKILL.md) | Conventional Commit + AI trailer + Hub PR conventions (incl. cross-repo coordination). | @@ -81,10 +81,12 @@ These Hub workflows aren't needed yet; create the skill when the packet that nee | Skill | Owning packet | Mirror of | | -------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------- | -| `add-provider-adapter` | P02c-2 (`LearnStackApiClient`) / P02c-5 (Let's Encrypt) / Phase 09b (Stripe, Iyzico) | LearnStack `add-provider-adapter` | +| `add-provider-adapter` | P02c-2 (`LearnStackApiClient`) / P02c-5 (Let's Encrypt) / Phase 09b (Stripe, Iyzico) | [LearnStack `add-provider-adapter`](https://github.com/HodeTech/LearnStack/blob/main/.claude/skills/add-provider-adapter/SKILL.md) | | `add-hub-permission` | P02c-4 (Operators module) | LearnStack `add-permission` (operator-scope only) | | `add-audit-coverage` | P02c-4 (Audit module) | LearnStack `add-audit-coverage` | | `add-operator-portal-route` / `add-i18n-key` | P02c-4 (operator portal) | LearnStack `add-frontend-route` / `add-i18n-key` | +| `add-recurring-job` | P02c-5 (challenge verification + renewal) / Hub Billing (usage rollup) | **No LearnStack mirror — Hub-originated.** The scheduler choice is an open `HUB-NNNN` ADR | +| `add-contract-test` | P02c-2 (`entitlement-v1.schema.json` + the OpenAPI/SDK contract) | **No LearnStack mirror.** Covers the byte-identical-across-repos rule and how it is checked in a coordinated PR | ## Authoring a new skill @@ -103,5 +105,5 @@ Body structure: **Purpose**, **When to use** / **When not to use**, **Inputs**, ## What skills are not - **Not duplicates of standards.** A skill is a _workflow_. LearnStack's [Standards corpus](https://github.com/HodeTech/LearnStack/blob/main/docs/standards/) is the authority; skills cite it. -- **Not decisions.** Decisions live in ADRs (LearnStack `../LearnStack/docs/decisions/` for cross-cutting; this repo's `docs/decisions/` for `HUB-NNNN`). +- **Not decisions.** Decisions live in ADRs ([LearnStack's `docs/decisions/`](https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/) for cross-cutting; this repo's `docs/decisions/` for `HUB-NNNN`). - **Not scratch space.** Exploratory notes go in `docs/analysis/` (gitignored). diff --git a/.claude/skills/add-feature-key/SKILL.md b/.claude/skills/add-feature-key/SKILL.md index 6357c4d..c20614d 100644 --- a/.claude/skills/add-feature-key/SKILL.md +++ b/.claude/skills/add-feature-key/SKILL.md @@ -46,7 +46,7 @@ public static readonly LimitKey = new("limits."); ### Step 3 — Keep it aligned with LearnStack core -The wire-format string **must** match LearnStack core's `FeatureKeys`/`LimitKeys` registry (`../LearnStack/backend/src/LearnStack.SharedKernel/FeatureFlags/`) exactly — LearnStack core reads the key from the projection by string. A mismatch means LearnStack silently never sees the feature. If you add a key Hub authors but LearnStack core doesn't yet read, note the pending LearnStack-side addition (a cross-repo registry-sync follow-up; the durable fix is a shared `LearnStack.Contracts` package, Phase 11 — see [plans.md § Registry sync](../../../docs/modules/plans.md)). +The wire-format string **must** match LearnStack core's `FeatureKeys` / `LimitKeys` registry exactly. That registry does **not exist yet** — it lands with [LearnStack Phase 02a Packet 9](https://github.com/HodeTech/LearnStack/blob/main/docs/roadmap/phase-02a-kernel-tenancy.md), so until then the default action is to record the pending LearnStack-side addition rather than to cross-check. When it exists it will live under [`https://github.com/HodeTech/LearnStack/blob/main/backend/src/LearnStack.SharedKernel/`](https://github.com/HodeTech/LearnStack/blob/main/backend/src/LearnStack.SharedKernel/) — LearnStack core reads the key from the projection by string. A mismatch means LearnStack silently never sees the feature. If you add a key Hub authors but LearnStack core doesn't yet read, note the pending LearnStack-side addition (a cross-repo registry-sync follow-up; the durable fix is a shared `LearnStack.Contracts` package, Phase 11 — see [plans.md § Registry sync](../../../docs/modules/plans.md)). ### Step 4 — Plan validator diff --git a/.claude/skills/add-integration-event/SKILL.md b/.claude/skills/add-integration-event/SKILL.md index 09cd39d..62c95d9 100644 --- a/.claude/skills/add-integration-event/SKILL.md +++ b/.claude/skills/add-integration-event/SKILL.md @@ -8,8 +8,9 @@ description: > learnstack.hub.custom-domain.activated), wiring a module to publish it via IOutbox.EnqueueAsync, or consuming a LearnStack-emitted event (learnstack.tenancy.*). DO NOT USE FOR: intra-module domain events (plain MediatR INotification, in-process), - the four HTTPS contract endpoints (those are request/response, not pub/sub), or - adding a fifth HTTPS contract endpoint (needs an ADR). + the HTTPS contract surface (those are request/response, not pub/sub), or adding an + endpoint to it (needs a LearnStack ADR — the surface is governed by ADR-0034's two + invariants, not by a count). --- # Adding a Hub integration event @@ -29,7 +30,9 @@ Wire a cross-boundary event the right way: outbox-written in the same transactio ## When not to use - Intra-module notification → plain MediatR `INotification` (`IDomainEvent`), in-process, same transaction. No outbox. -- A request/response contract → that's one of the four HTTPS endpoints (a fifth needs an ADR in `../LearnStack/docs/decisions/`). +- A request/response contract → that belongs to the HTTPS contract surface enumerated in + [ADR-0034 § The endpoint set](https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/0034-hub-contract-surface-invariant.md); + adding one needs a LearnStack ADR. ## Workflow @@ -80,4 +83,7 @@ For a LearnStack-emitted event Hub consumes (e.g. `learnstack.tenancy.tenant.ren - **An unversioned event.** Breaking changes need `V2`. - **Wrong topic prefix.** Hub publishes under `learnstack.hub.*`; never under bare `learnstack.*`. - **A consumer without the inbox guard.** Re-delivery duplicates the side effect. -- **Reaching for a fifth HTTPS endpoint instead of an event.** If it's fire-and-forget cross-boundary state, it's an event; a new request/response endpoint needs an ADR. +- **Reaching for a new HTTPS endpoint instead of an event.** If it's fire-and-forget + cross-boundary state, it's an event; a new request/response endpoint needs a LearnStack + ADR. ADR-0034 retired the "closed at four endpoints" framing — protecting that count is + what caused TLS private keys to be tunnelled through the entitlement payload. diff --git a/.claude/skills/code-review/SKILL.md b/.claude/skills/code-review/SKILL.md index 1655ffc..41b614e 100644 --- a/.claude/skills/code-review/SKILL.md +++ b/.claude/skills/code-review/SKILL.md @@ -59,7 +59,7 @@ Walk all five. Cite `file:line` for every finding; classify Blocker / Major / Mi - Small single-responsibility methods; descriptive names; no dead code / commented-out blocks; no undated TODO. - No premature abstraction (three similar lines beat a premature helper); no half-finished implementation. -- The change mirrors the LearnStack pattern it's based on (open `../LearnStack/backend/src/...` and compare) rather than improvising a parallel shape. +- The change mirrors the pattern it's based on. Compare against the Hub's own shipped source first (`backend/src/Core/`, `backend/src/Modules/`); for a LearnStack-only pattern read [`https://github.com/HodeTech/LearnStack/blob/main/backend/src/`](https://github.com/HodeTech/LearnStack/blob/main/backend/src/) — no sibling checkout needed. ### 5. Hub-structural (the delta lens) @@ -73,7 +73,7 @@ When delegating to a second agent, compose a self-contained prompt that: - Sets Hub context + **the five Hub deltas** ([../README.md](../README.md)). - Names the commit / branch / file list under review (`git log --oneline`, `git show --stat`). -- Points at the LearnStack-side authority (`../LearnStack/docs/...`) the change derives from. +- Points at the LearnStack-side authority it derives from, by absolute URL ([`https://github.com/HodeTech/LearnStack/blob/main/docs/`](https://github.com/HodeTech/LearnStack/blob/main/docs/)). - Tells the agent to walk all five lenses above. - Defines the output: verdict + findings as Blocker / Major / Minor / Suggestion with `file:line` + recommendation. - Insists on reading the surrounding Hub docs (`docs/architecture/`, `docs/modules/`) — don't review in isolation. diff --git a/.claude/skills/commit-and-pr/SKILL.md b/.claude/skills/commit-and-pr/SKILL.md index 3349b68..a39a8c7 100644 --- a/.claude/skills/commit-and-pr/SKILL.md +++ b/.claude/skills/commit-and-pr/SKILL.md @@ -63,7 +63,7 @@ Feature branch `feat/phase-02c-packet-N-`. Push with `-u` only when the us ### Step 5 — Cross-repo coordination (if the packet spans both repos) -Per [CLAUDE.md § Cross-repo coordination](../../../CLAUDE.md): the Hub-side PR opens first (it carries the canonical contract shape); the LearnStack-side PR references the Hub PR's commit hash; both merge in the same session. Adding/changing a contract endpoint requires a new ADR in `../LearnStack/docs/decisions/` first. **Do not** push or merge anything in `../LearnStack` without explicit user permission — another agent may be active there. +Per [CLAUDE.md § Cross-repo coordination](../../../CLAUDE.md): the Hub-side PR opens first (it carries the canonical contract shape); the LearnStack-side PR references the Hub PR's commit hash; both merge in the same session. Adding/changing a contract endpoint requires a new ADR in the `HodeTech/LearnStack` repo under `docs/decisions/` first, merged ahead of both code PRs. **Do not** push or merge anything in `../LearnStack` without explicit user permission — another agent may be active there. ## Validation @@ -76,5 +76,5 @@ Per [CLAUDE.md § Cross-repo coordination](../../../CLAUDE.md): the Hub-side PR - **Pushing without being asked.** Default is local commit. - **Amending / force-pushing `main`.** Forbidden — new commits only. -- **A contract change without an ADR.** Adding or reshaping an endpoint on the contract surface needs an ADR in `../LearnStack/docs/decisions/` first — it is a cross-repository agreement (ADR-0034). +- **A contract change without an ADR.** Adding or reshaping an endpoint on the contract surface needs an ADR in the `HodeTech/LearnStack` repo under `docs/decisions/`, merged first — it is a cross-repository agreement ([ADR-0034](https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/0034-hub-contract-surface-invariant.md)). - **Touching `../LearnStack` branch state from a Hub session.** Coordinate; don't interfere with a parallel agent. diff --git a/.claude/skills/implement-task/SKILL.md b/.claude/skills/implement-task/SKILL.md index ed6746e..990c1a5 100644 --- a/.claude/skills/implement-task/SKILL.md +++ b/.claude/skills/implement-task/SKILL.md @@ -45,11 +45,14 @@ The ten steps are mandatory; skipping any is the bug this skill prevents. ### Step 1 — Scope and alignment -Run [start-task](../start-task/SKILL.md): read the right docs in order (Hub docs under `docs/`, then LearnStack authority under `../LearnStack/docs/`), confirm phase fit against `docs/roadmap/README.md`, walk the [CLAUDE.md hard rules](../../../CLAUDE.md), and pick the specific workflow skill(s) you'll invoke ([add-hub-module](../add-hub-module/SKILL.md), [add-hub-aggregate](../add-hub-aggregate/SKILL.md), [add-mediatr-handler](../add-mediatr-handler/SKILL.md), [wire-cross-cutting-foundation](../wire-cross-cutting-foundation/SKILL.md), …). Output: a one-paragraph problem statement in your own words, the packet it belongs to, the standards that govern it, the skill(s) you'll use. +Run [start-task](../start-task/SKILL.md): read the right docs in order (Hub docs under `docs/`, then LearnStack authority at [`https://github.com/HodeTech/LearnStack/blob/main/docs/`](https://github.com/HodeTech/LearnStack/blob/main/docs/) — read it on GitHub, no sibling checkout needed), confirm phase fit against `docs/roadmap/README.md`, walk the [CLAUDE.md hard rules](../../../CLAUDE.md), and pick the specific workflow skill(s) you'll invoke ([add-hub-module](../add-hub-module/SKILL.md), [add-hub-aggregate](../add-hub-aggregate/SKILL.md), [add-mediatr-handler](../add-mediatr-handler/SKILL.md), [wire-cross-cutting-foundation](../wire-cross-cutting-foundation/SKILL.md), …). Output: a one-paragraph problem statement in your own words, the packet it belongs to, the standards that govern it, the skill(s) you'll use. ### Step 2 — Inspect and understand -Read every file the change touches **before** editing. Trace one hop out (who calls this, who reads this table, what events flow). Read the relevant Hub design spec (`docs/architecture/*.md`, `docs/modules/*.md`) and the LearnStack-side ADR/standard it derives from. If `git log` shows recent edits, read the commit messages for direction. **Mirror, don't invent:** if a pattern exists in `../LearnStack/backend/src/`, open it and reproduce it (adjusting for the Hub deltas), rather than improvising. +Read every file the change touches **before** editing. Trace one hop out (who calls this, who reads this table, what events flow). Read the relevant Hub design spec (`docs/architecture/*.md`, `docs/modules/*.md`) and the LearnStack-side ADR/standard it derives from. If `git log` shows recent edits, read the commit messages for direction. **Mirror, don't invent:** the Hub's own `backend/src/Core/` and `backend/src/Modules/` are +the first place to look — four modules and the SharedKernel are already on `main`. For a +pattern that exists only in LearnStack, read it at +[`https://github.com/HodeTech/LearnStack/blob/main/backend/src/`](https://github.com/HodeTech/LearnStack/blob/main/backend/src/) rather than assuming a local checkout. ### Step 3 — Plan @@ -69,7 +72,7 @@ Use [run-tests-locally](../run-tests-locally/SKILL.md): `~/.dotnet/dotnet build ### Step 7 — Update every related document -Walk the list, leave nothing stale: `docs/roadmap/README.md` (packet status); the affected `docs/modules/.md` / `docs/architecture/*.md`; `docs/glossary.md` (new term → [update-glossary](../update-glossary/SKILL.md)); a Hub-internal ADR if a new rule emerged → [write-adr](../write-adr/SKILL.md); the sibling-link audit (CI's `meta` job pattern — `(\.\./)+learnstack/` links are validated locally only). +Walk the list, leave nothing stale: `docs/roadmap/README.md` (packet status); the affected `docs/modules/.md` / `docs/architecture/*.md`; `docs/glossary.md` (new term → [update-glossary](../update-glossary/SKILL.md)); a Hub-internal ADR if a new rule emerged → [write-adr](../write-adr/SKILL.md); the link audit (CI's `meta` job **fails** on a sibling-relative `../LearnStack/...` Markdown link; use the absolute GitHub URL). ### Step 8 — Commit diff --git a/.claude/skills/local-dev-setup/SKILL.md b/.claude/skills/local-dev-setup/SKILL.md index 718f255..2ebe8fb 100644 --- a/.claude/skills/local-dev-setup/SKILL.md +++ b/.claude/skills/local-dev-setup/SKILL.md @@ -40,7 +40,7 @@ Get the Hub stack running. Hub's compose is deliberately minimal — it runs onl ### Step 1 — Bootstrap (one-time) ```bash -cd ~/Documents/Projects/learnstack-hub +cd ~/Documents/Projects/LearnStack-Hub make install # restores backend NuGet + frontend pnpm + activates .githooks/pre-commit ``` @@ -63,7 +63,12 @@ Brings up the Hub-only services: Dapr placement (50006), Dapr sidecar (3501/5000 ### Step 4 — Hub API ```bash -cd ../LearnStack-Hub/backend +# The Api host does not read .env — export it the way scripts/seed.sh does, +# or the connection string falls back to defaults and Postgres is unreachable. +cd ../LearnStack-Hub +set -a; . ./.env; set +a +export POSTGRES_HOST=localhost +cd backend ~/.dotnet/dotnet run --project src/Core/LearnStack.Hub.Api # binds 0.0.0.0:5181 ``` diff --git a/.claude/skills/standards-check/SKILL.md b/.claude/skills/standards-check/SKILL.md index f48c651..098726a 100644 --- a/.claude/skills/standards-check/SKILL.md +++ b/.claude/skills/standards-check/SKILL.md @@ -61,7 +61,12 @@ Walk these against the diff. Each item is pass/fail; a fail blocks merge until f ### Contract surface + boundary ([CLAUDE.md hard rules](../../../CLAUDE.md)) -- [ ] No fifth Hub HTTPS endpoint without an ADR in `../LearnStack/docs/decisions/`. +- [ ] The Hub stores no tenant content (`Hub_NeverStores_TenantData`), and every + LearnStack↔Hub crossing goes through `IEntitlementProvider` / `IUsageReporter` / + `IHubTenantSync` — nothing else holds a Hub client. Those are + [ADR-0034](https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/0034-hub-contract-surface-invariant.md)'s two invariants; + the surface is **not** governed by an endpoint count. Any endpoint added to the set + needs a merged LearnStack ADR first. - [ ] `/api/internal/*` not internet-exposed (when those endpoints land). - [ ] No Kubernetes-credential / K8s-state writes to LearnStack's cluster. - [ ] `learnstack-hub` realm boundary respected. @@ -71,7 +76,10 @@ Walk these against the diff. Each item is pass/fail; a fail blocks merge until f ### Docs + corpus hygiene - [ ] Adjacent docs updated (module deep dive, glossary, roadmap status, ADR cross-link). -- [ ] Sibling-relative links (`(\.\./)+learnstack/...`) resolve locally. +- [ ] **No** sibling-relative Markdown link to LearnStack (`../LearnStack/...`) anywhere — + cross-repo references are absolute `https://github.com/HodeTech/LearnStack/blob/main/...` URLs, and CI's `meta` job **rejects** the + relative form. `../LearnStack` stays correct for shell paths only. Every other relative + link resolves from its own file's directory. - [ ] English docs; Mermaid diagrams readable as text. ## Output diff --git a/.claude/skills/start-task/SKILL.md b/.claude/skills/start-task/SKILL.md index 94b8075..2405fab 100644 --- a/.claude/skills/start-task/SKILL.md +++ b/.claude/skills/start-task/SKILL.md @@ -33,7 +33,9 @@ Orient correctly before writing code: read the right docs, confirm the work fits 1. [README.md](../../../README.md) — direction at a glance. 2. The relevant Hub design spec: `docs/architecture/module-topology.md`, `docs/architecture/cross-cutting-foundation.md`, `docs/architecture/entitlement-projection.md`, and the `docs/modules/.md` for the module(s) in scope. -3. The LearnStack-side authority the spec derives from: the cited ADR(s) under `../LearnStack/docs/decisions/` and standard(s) under `../LearnStack/docs/standards/`. +3. The LearnStack-side authority the spec derives from: the cited ADR(s) at + [https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/](https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/) and standard(s) at + [https://github.com/HodeTech/LearnStack/blob/main/docs/standards/](https://github.com/HodeTech/LearnStack/blob/main/docs/standards/) — read on GitHub; no sibling checkout needed. 4. [docs/roadmap/README.md](../../../docs/roadmap/README.md) — which packet owns this work. 5. [docs/glossary.md](../../../docs/glossary.md) — terms. @@ -41,7 +43,16 @@ Read, don't skim. If `git log` shows recent edits to the surface, read the commi ### Step 2 — Check packet fit -Confirm the task belongs to the current/next packet (`docs/roadmap/README.md`). If it belongs to a later packet, say so and stop — don't pull future work forward. If it requires LearnStack-side changes, flag it as a cross-repo coordinated packet (it's not Hub-only). +**The Hub track is frozen from P02c-2 onward** (owner decision 2026-08-08). Before +anything else, check the task against [CLAUDE.md § What state this is in](../../../CLAUDE.md) +and [the freeze](../../../docs/roadmap/README.md): if it belongs to P02c-2 or any packet +after it, **say so and stop** — the two conditions that resume the track are named there, +and neither has fired. + +If it is not frozen, confirm it belongs to the current packet (`docs/roadmap/README.md`). +If it belongs to a later packet, say so and stop — don't pull future work forward. If it +requires LearnStack-side changes, flag it as a cross-repo coordinated packet (it's not +Hub-only) and follow the two-PR protocol. ### Step 3 — Walk the Hub deltas + hard rules diff --git a/.claude/skills/update-glossary/SKILL.md b/.claude/skills/update-glossary/SKILL.md index e965730..5cbfcbe 100644 --- a/.claude/skills/update-glossary/SKILL.md +++ b/.claude/skills/update-glossary/SKILL.md @@ -6,7 +6,7 @@ description: > Hub APISIX, learnstack-hub realm, etc.) that other Hub docs cite; fixing a stale entry; removing a deprecated term. DO NOT USE FOR: cross-cutting terms shared with LearnStack core (Tenant, Organization, IModule, DeploymentMode — those live in - ../LearnStack/docs/glossary.md; link to them), general programming terms, or terms + https://github.com/HodeTech/LearnStack/blob/main/docs/glossary.md; link to them), general programming terms, or terms only used inside docs/analysis/ (gitignored). --- @@ -23,7 +23,7 @@ Keep `docs/glossary.md` the single source of truth for Hub-specific terminology. ## When not to use -- The term is cross-cutting (shared with LearnStack core) → it lives in `../LearnStack/docs/glossary.md`; link to it, don't duplicate. +- The term is cross-cutting (shared with LearnStack core) → it lives in [LearnStack's glossary](https://github.com/HodeTech/LearnStack/blob/main/docs/glossary.md); link to it, don't duplicate. - General programming term → link to its canonical source. - Term used only in `docs/analysis/` → not glossary-worthy. diff --git a/.claude/skills/wire-cross-cutting-foundation/SKILL.md b/.claude/skills/wire-cross-cutting-foundation/SKILL.md index 0c9013a..3588a9f 100644 --- a/.claude/skills/wire-cross-cutting-foundation/SKILL.md +++ b/.claude/skills/wire-cross-cutting-foundation/SKILL.md @@ -29,14 +29,21 @@ Stand up the Hub's foundation so domain code programs against the same `Result` audit columns, `CapturedContext`). There is no tenant `UserId` in Hub. - **`HubException`** replaces `LearnStackException` as the base exception (namespace `LearnStack.Hub.SharedKernel.Errors`); `DomainException` / `InfrastructureException` / `ProviderException` derive from it. -Surface to reproduce (see [docs/architecture/cross-cutting-foundation.md § 1](../../../docs/architecture/cross-cutting-foundation.md)): `Results/`, `Localization/`, `Domain/`, `Identifiers/`, `Time/`, `Random/`, `Pagination/`, `Persistence/`, `Errors/`, `Secrets/`, `Observability/`, `Resilience/`, `Hosting/` (`DeploymentMode`), `FeatureFlags/` (`FeatureKey`/`LimitKey` — see [add-feature-key](../add-feature-key/SKILL.md)), and `LearnStackHubVogenDefaults.IdMask`. Open the live LearnStack source for the exact public API shapes — reproduce them verbatim. +Surface to reproduce (see [docs/architecture/cross-cutting-foundation.md § 1](../../../docs/architecture/cross-cutting-foundation.md)): `Results/`, `Localization/`, `Domain/`, `Identifiers/`, `Time/`, `Random/`, `Pagination/`, `Persistence/`, `Errors/`, `Secrets/`, `Observability/`, `Resilience/`, `Hosting/` (`DeploymentMode`), `FeatureFlags/` (`FeatureKey`/`LimitKey` — see [add-feature-key](../add-feature-key/SKILL.md)), and `LearnStackHubVogenDefaults.IdMask`. For the exact public API shapes, the Hub's own shipped source is authoritative — it is what the four modules compile against. LearnStack's original is the **reconciliation** reference only, at [`https://github.com/HodeTech/LearnStack/blob/main/backend/src/LearnStack.SharedKernel/`](https://github.com/HodeTech/LearnStack/blob/main/backend/src/LearnStack.SharedKernel/). `.csproj`: `Vogen` (PrivateAssets="all"), `MediatR`, `Microsoft.EntityFrameworkCore`, `Polly`, `Microsoft.Extensions.Configuration.Abstractions` — match LearnStack's SharedKernel.csproj. Add unit tests mirroring LearnStack's (Result, LocalizedMessage prefix invariant, Entity equality, FixedClock, generation-style invariants). @@ -92,4 +99,4 @@ Mirroring LearnStack's `DomainExceptionThrowAnalyzer` as `LearnStack.Hub.Analyze - **Leaving `UserId` in the mirror.** Substitute `OperatorId` everywhere. - **Registering the OTel LoggerProvider alongside Serilog.** Double-exports every log line. - **Reading `DeploymentMode` inside a module.** Branch once at the composition root. -- **Improvising shapes.** Open `../LearnStack/backend/src/LearnStack.SharedKernel/` + `LearnStack.Api/Common/` + `LearnStack.Application/Pipeline/` and reproduce. +- **Improvising shapes.** The Hub's shipped `backend/src/Core/LearnStack.Hub.SharedKernel/`, `LearnStack.Hub.Api/Common/` and `LearnStack.Hub.Application/Pipeline/` are the authority — the four modules compile against them. Reconcile against [LearnStack's originals](https://github.com/HodeTech/LearnStack/blob/main/backend/src/) only when closing a named drift. diff --git a/.claude/skills/write-adr/SKILL.md b/.claude/skills/write-adr/SKILL.md index 9f36916..98b72c2 100644 --- a/.claude/skills/write-adr/SKILL.md +++ b/.claude/skills/write-adr/SKILL.md @@ -7,7 +7,8 @@ description: > background-job orchestration, operator-portal-specific pattern). DO NOT USE FOR: cross-cutting decisions that touch the Hub↔LearnStack contract, entitlement projection shape, custom-domain lifecycle, two-realm boundary, or deployment model - (those are LearnStack ADRs — file in ../LearnStack/docs/decisions/), editing an + (those are LearnStack ADRs — filed in the `HodeTech/LearnStack` repo under + `docs/decisions/`, as a coordinated pull request), editing an Accepted ADR's decision section (write a superseding ADR), or day-to-day choices (those go in code review / commit messages). --- @@ -25,7 +26,7 @@ Capture a Hub-only architectural decision durably, with its drivers and the opti ## When not to use -- The decision touches the Hub↔LearnStack contract / entitlement shape / custom-domain lifecycle / two-realm boundary / deployment model → it's a **LearnStack** ADR; file under `../LearnStack/docs/decisions/` (coordinated, with user permission). +- The decision touches the Hub↔LearnStack contract / entitlement shape / custom-domain lifecycle / two-realm boundary / deployment model → it's a **LearnStack** ADR; it is filed in the `HodeTech/LearnStack` repo under `docs/decisions/` as its own pull request, which **merges before** either code PR opens (coordinated, with user permission). Template and numbering: [https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/](https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/). - Editing an Accepted ADR's Decision section → write a new ADR that supersedes it. - A routine implementation choice → commit message / code review. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb51a84..fb71852 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,7 @@ # `apps/operator-portal` + `packages/{config,sdk,ui}`. # - `backend-integration` runs from P02c-1, which landed the first # Testcontainers-backed tests (the entitlement-rebuild round trip). -# - `openapi-diff` job is `if: false` until P02c-2 produces the first +# - `openapi-diff` is gated on an unset `vars.ENABLE_OPENAPI_DIFF` until P02c-2 produces the first # generated OpenAPI spec. # - No `lighthouse-budget` job — operator portal is internal-only; the # accessibility / performance budget review happens manually in Phase @@ -188,13 +188,17 @@ jobs: openapi-diff: name: openapi diff (deferred to P02c-2) runs-on: ubuntu-latest - if: false # activate when /api/v1/internal/license/verify replaces /healthz as the only documented surface + # Disabled by default: an unset repository variable is the empty string, so this is + # false until it is set to 'true'. Not `if: false` — actionlint rejects a constant + # condition ([if-cond]). Activates when /api/v1/internal/license/verify replaces + # /healthz as the only documented surface. + if: vars.ENABLE_OPENAPI_DIFF == 'true' steps: - run: echo "Placeholder — P02c-2 wires oasdiff against the prior main spec." # ─── Meta (commit-message format, link audit) ───────────────────────── meta: - name: meta (commit hygiene + link audit) + name: meta (markdown link audit) runs-on: ubuntu-latest timeout-minutes: 5 steps: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 96dc69f..b0d5646 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,8 @@ Required status checks on `main`: - `backend` — `dotnet build` + format verify + unit + architecture + contract tests - `frontend` — pnpm install + typecheck + lint + build + Vitest -- `meta` — `make lint`-style format verification + Markdown link audit +- `meta` — Markdown link audit on changed docs. Backend format verification runs in + `backend` (`dotnet format --verify-no-changes`); frontend lint runs in `frontend`. - `secret-scan` — Leakwatch scan (gates per LearnStack Standards 12 § Secrets Management) `backend-integration` runs from **P02c-1**, which landed the first Testcontainers-backed tests (the entitlement-rebuild round trip). It was gated `if: false` from P02c-0 until then. diff --git a/README.md b/README.md index fc4b60e..2161d8f 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,13 @@ cd ../LearnStack/backend dotnet run --project src/LearnStack.Api # 4. Hub API -cd ../LearnStack-Hub/backend -dotnet run --project src/LearnStack.Hub.Api +# The host does not load .env itself, so export it first — otherwise the +# connection string resolves to defaults and the API cannot reach Postgres. +cd ../LearnStack-Hub +set -a; . ./.env; set +a +export POSTGRES_HOST=localhost +cd backend +dotnet run --project src/Core/LearnStack.Hub.Api ``` In production, the two repos deploy independently — the shared compose is a dev-time convenience only. diff --git a/backend/src/Core/LearnStack.Hub.Infrastructure/Composition/HubFoundationRegistration.cs b/backend/src/Core/LearnStack.Hub.Infrastructure/Composition/HubFoundationRegistration.cs index 33611a0..3cfe3f7 100644 --- a/backend/src/Core/LearnStack.Hub.Infrastructure/Composition/HubFoundationRegistration.cs +++ b/backend/src/Core/LearnStack.Hub.Infrastructure/Composition/HubFoundationRegistration.cs @@ -79,7 +79,10 @@ public static string ResolveConnectionString(IConfiguration configuration) var host = configuration["POSTGRES_HOST"] ?? "localhost"; var port = configuration["POSTGRES_PORT"] ?? "5432"; - var database = configuration["POSTGRES_DB_HUB"] ?? "learnstack_hub"; + // Key matches .env.example and infra/compose/dev.yml, which provision + // ${HUB_POSTGRES_DB}. Reading a different key worked only because both + // sides defaulted to the same literal. + var database = configuration["HUB_POSTGRES_DB"] ?? "learnstack_hub"; var username = configuration["POSTGRES_USER"] ?? "learnstack"; var password = configuration["POSTGRES_PASSWORD"]; diff --git a/backend/tests/LearnStack.Hub.Tests.Architecture/HubBoundaryTests.cs b/backend/tests/LearnStack.Hub.Tests.Architecture/HubBoundaryTests.cs index 11165d2..2c84e55 100644 --- a/backend/tests/LearnStack.Hub.Tests.Architecture/HubBoundaryTests.cs +++ b/backend/tests/LearnStack.Hub.Tests.Architecture/HubBoundaryTests.cs @@ -5,14 +5,14 @@ namespace LearnStack.Hub.Tests.Architecture; /// /// Hub-specific boundary rules. The authoritative list of Hub-side architecture -/// tests is in `../LearnStack/docs/architecture/24-learnstack-hub.md` § 10 -/// (six tests): +/// tests is Architecture 24 § 10, at +/// https://github.com/HodeTech/LearnStack/blob/main/docs/architecture/24-learnstack-hub.md +/// (seven tests — ADR-0034 added Hub_Client_Referenced_Only_By_Named_Adapters): /// -/// 1. `Hub_NeverStores_TenantContent` (Architecture 24 spelling; ADR-0019 -/// § Architecture tests calls the same rule `Hub_NeverStores_TenantData`) -/// — see below. This repo uses -/// the ADR-0019 name because the ADR is the Accepted decision; the -/// drift is a corpus-side reconciliation belonging to LearnStack core. +/// 1. `Hub_NeverStores_TenantData` — see +/// below. Architecture 24 once spelled this `Hub_NeverStores_TenantContent`; +/// the 2026-08-08 restructure corrected it to the ADR-0019 name, so the two +/// documents now agree and this repo needs no local exception. /// 2. `Hub_Modules_DoNotReference_LearnStack_Internals` — lands in P02c-1 /// once Hub modules exist. /// 3. `Internal_API_Endpoints_AreNot_Public` — integration test; lands in @@ -32,14 +32,6 @@ namespace LearnStack.Hub.Tests.Architecture; /// - `LicenseKey_Validation_Is_Pinned_RSA2048` (ADR-0020) — P02c-6. /// - `Cert_PrivateKey_NeverLeavesVault_To_Logs` (ADR-0022) — P02c-5. /// - `CustomDomain_TenantId_NeverReadFrom_RequestBody` (ADR-0022) — P02c-5. -/// -/// KNOWN CORPUS DRIFT (not a Hub bug; flagged for the LearnStack-side -/// reconciliation packet): ADR-0019 § Architecture tests lists only four -/// rules and uses the name `Hub_NeverStores_TenantData`; Architecture 24 -/// § 10 lists six rules and uses `Hub_NeverStores_TenantContent`. Hub -/// follows ADR-0019's name (the Accepted decision is authority); P02c-3 -/// or a follow-up amendment to ADR-0019 should fold all six tests into the -/// ADR's § Architecture tests section so the two docs agree. /// public sealed class HubBoundaryTests { diff --git a/backend/tests/LearnStack.Hub.Tests.Architecture/RepositoryLayoutTests.cs b/backend/tests/LearnStack.Hub.Tests.Architecture/RepositoryLayoutTests.cs index d85c625..a4c1083 100644 --- a/backend/tests/LearnStack.Hub.Tests.Architecture/RepositoryLayoutTests.cs +++ b/backend/tests/LearnStack.Hub.Tests.Architecture/RepositoryLayoutTests.cs @@ -29,7 +29,8 @@ public void No_Source_Folder_Named_Verticals() offenders.Should().BeEmpty( "ADR-0018 (LearnStack core) applies to Hub by reference: tenant-specific shapes " + - "live as data, not code. See ../LearnStack/docs/decisions/0018-tenant-driven-customization-model.md."); + "live as data, not code. See https://github.com/HodeTech/LearnStack/blob/main/" + + "docs/decisions/0018-tenant-driven-customization-model.md."); } /// diff --git a/infra/compose/README.md b/infra/compose/README.md index faf39c1..a0483f2 100644 --- a/infra/compose/README.md +++ b/infra/compose/README.md @@ -24,7 +24,7 @@ cd ../LearnStack-Hub && make dev cd ../LearnStack/backend && dotnet run --project src/LearnStack.Api # 4. Hub API -cd ../LearnStack-Hub/backend && dotnet run --project src/LearnStack.Hub.Api +cd ../LearnStack-Hub/backend && dotnet run --project src/Core/LearnStack.Hub.Api ``` The boot-order dependency is dev-only. Production deploys the two stacks independently. From 4287d1f678505a144560941e30d5e60e433d2ea7 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Mon, 10 Aug 2026 21:09:19 +0300 Subject: [PATCH 2/3] docs(roadmap,architecture): three audit residues from the handoff review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The autonomy audit that scoped the handoff PR flagged three items as out-of-scope for it. They are corpus defects, not handoff gaps, so they land here rather than in a follow-up nobody owns. 1. `caps.value` is a string on the wire. The worked example in entitlement-projection.md carried `"value": 365` as a JSON number, which `entitlement-v1.schema.json` rejects — the envelope declares `"value": { "type": ["string", "null"] }` with `additionalProperties: false`, and `ComplianceCapDto.Value` is `string?`, so the serialiser cannot emit a number anyway. The document's own contract test would have failed on its own example. Corrected to `"365"` and stated as a rule, because the next numeric cap will reach for the same shape. 2. The dependency map was missing `p4 → p5` and `p4 → p6`. Both P02c-5 and P02c-6 ship "Operator portal screens" sections that build on the portal shell and its Operators/Audit modules; the prose said so, the graph did not. A reader planning parallel work off the diagram would have scheduled them concurrently. 3. P02c-4 was labelled "touches no LearnStack code" in two places while its own Deliverables require editing `../LearnStack/infra/keycloak/realms/learnstack-hub.json` — the operator roles and the MFA-required browser flow — and describe it as a coordinated cross-repo change. The file cannot be duplicated into this repository because LearnStack's compose imports both realms at first boot. P02c-4 now carries a row in "Hub waits on LearnStack" and is counted with the coordinated packets, leaving P02c-2 as the only fully Hub-local packet remaining. The third is the failure mode this whole review keeps finding: a fact was corrected where the work was being done and left stale in the index that summarises it. A packet declared LearnStack-free is a packet whose cross-repo edit nobody schedules. Co-Authored-By: Claude Opus 5 (1M context) --- docs/architecture/entitlement-projection.md | 10 +++++++++- docs/roadmap/README.md | 12 +++++++++++- docs/roadmap/p02c-4-operator-portal.md | 8 +++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/architecture/entitlement-projection.md b/docs/architecture/entitlement-projection.md index 9b744e0..6846a5d 100644 --- a/docs/architecture/entitlement-projection.md +++ b/docs/architecture/entitlement-projection.md @@ -37,7 +37,7 @@ This is the JSON the projection serialises to (per [Architecture 24 § 4](https: "compliance": { "caps": { "gdpr.hard_delete.enabled": { "allowed": true, "forced": false }, - "audit.retention.days": { "allowed": true, "forced": true, "value": 365 }, + "audit.retention.days": { "allowed": true, "forced": true, "value": "365" }, "data.residency.region": { "allowed": false, "forced": true, @@ -56,6 +56,14 @@ This is the JSON the projection serialises to (per [Architecture 24 § 4](https: - **Feature keys** use the dotted snake_case form with **no `.enabled` suffix** (dropped in [ADR-0021 Amendment 1](https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/0021-feature-based-entitlement.md)). Every feature is implicitly boolean. e.g. `classroom.recording`, `tenancy.custom_domain`, `identity.sso.saml`. - **Limit keys** carry the `limits.` prefix; value `-1` = unlimited, `0` = not available. e.g. `limits.max_users`. - **Compliance-cap keys** keep their own `.enabled` portion as part of the cap name (it is NOT a redundant suffix), and the value is a `{ allowed, forced, value? }` object — not a bare bool. +- Inside that object, **`value` is always a string on the wire, or null** — never a + number. A numeric cap such as `audit.retention.days` is carried as `"365"` and parsed + by the consumer. `entitlement-v1.schema.json` declares + `"value": { "type": ["string", "null"] }` and the envelope is + `additionalProperties: false`, so a numeric literal is rejected outright — and + `ComplianceCapDto.Value` is `string?`, so the serialiser cannot emit one anyway. An + example here showing `365` unquoted described a payload that would fail its own + contract test. - The `tier` mirrors the `Plan.tier` (`starter | growth | scale | enterprise | custom`). P02c-1 stores `features` / `limits` / `compliance_caps` as **JSONB columns** on the `entitlements` table. The projection serialiser produces exactly the JSON above. diff --git a/docs/roadmap/README.md b/docs/roadmap/README.md index fd73032..ebbbe88 100644 --- a/docs/roadmap/README.md +++ b/docs/roadmap/README.md @@ -100,6 +100,8 @@ flowchart TB p2 -- "OpenAPI + generated SDK" --> p4 p2 -- "outbound client for host-mappings push" --> p5 p2 -- "verify endpoint the .lic path calls" --> p6 + p4 -- "portal shell + Operators + Audit modules" --> p5 + p4 -- "portal shell + operator audit" --> p6 p3 --> p7 p4 --> p7 p5 --> p7 @@ -130,11 +132,19 @@ Both repositories block each other in places. Neither table is a wish list — e | P02c-3 | [P02a-7 Resolution + isolation](https://github.com/HodeTech/LearnStack/blob/main/docs/roadmap/phase-02a-kernel-tenancy.md) | `IHostToTenantResolver`, `TenantResolverMiddleware`, and the `HubCorrelationMiddleware` seam that populates `ITenantContext` on `/api/internal/*` | | P02c-3 | [P02a-9 Audit + entitlement socket](https://github.com/HodeTech/LearnStack/blob/main/docs/roadmap/phase-02a-kernel-tenancy.md) | The `IEntitlementProvider` socket with `NullEntitlementProvider` as its only implementation | | P02c-3 | [Phase 02b Identity Integration + Events](https://github.com/HodeTech/LearnStack/blob/main/docs/roadmap/phase-02b-events-auth.md) | The `OutboxProcessor` and its claim protocol, `IInboxGuard` and the per-module `inbox_messages` tables, and handler-scope tenant-context restoration. `IUsageReporter` dispatches through the outbox rather than inline, and the `learnstack.hub.entitlement` invalidation consumer is an ordinary `IIntegrationEventHandler` behind the same inbox guard | +| P02c-4 | [LearnStack `infra/keycloak/realms/learnstack-hub.json`](https://github.com/HodeTech/LearnStack/blob/main/infra/keycloak/realms/learnstack-hub.json) | The operator roles and the MFA-required browser flow, added to the single owning realm export — LearnStack's compose imports both realms at first boot, so the file cannot be duplicated here. A coordinated pull request; the rest of P02c-4 is Hub-only | | P02c-6 | [P02a-9 Audit + entitlement socket](https://github.com/HodeTech/LearnStack/blob/main/docs/roadmap/phase-02a-kernel-tenancy.md) | The `IEntitlementProvider` socket the LearnStack-side `SignedLicenseKeyEntitlementProvider` skeleton plugs into, in a coordinated pull request | | P02c-5 | [LearnStack Phase 02c](https://github.com/HodeTech/LearnStack/blob/main/docs/roadmap/phase-02c-hub-foundation.md) | The LearnStack-side `host-mappings` handler and its `platform_host_to_tenant` mirroring — the paired half of this packet, merged in the same session | | P02c-5 | [LearnStack Phase 11](https://github.com/HodeTech/LearnStack/blob/main/docs/roadmap/phase-11-production-hardening.md) | The LearnStack **edge** half only: certificate installation at the gateway, demand-gated per [ADR-0035](https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/0035-demand-gated-infrastructure.md). P02c-5 does **not** wait on it — host resolution works from the `platform_host_to_tenant` row alone | -P02c-0 and P02c-1 have shipped. Of what remains, **P02c-2 and P02c-4 are unblocked by LearnStack** — they touch no LearnStack code and can proceed as soon as the Hub track resumes. P02c-3, P02c-5 and P02c-6 each land as two coordinated pull requests; see the tables above and [Coordination protocol](#coordination-protocol). P02c-3 is the only packet gated on the LearnStack spine reaching **Phase 02b**, not merely Phase 02a. +P02c-0 and P02c-1 have shipped. Of what remains, **only P02c-2 is fully Hub-local** — it +touches no LearnStack code and can proceed as soon as the Hub track resumes. **P02c-3, +P02c-4, P02c-5 and P02c-6 each land as two coordinated pull requests**; see the tables +above and [Coordination protocol](#coordination-protocol). P02c-4's cross-repo half is +the smallest of the four — the operator roles and MFA browser flow in the realm export — +but it is not zero, and calling the packet LearnStack-free is what let that edit go +unplanned. P02c-3 is the only packet gated on the LearnStack spine reaching **Phase +02b**, not merely Phase 02a. ### LearnStack waits on Hub diff --git a/docs/roadmap/p02c-4-operator-portal.md b/docs/roadmap/p02c-4-operator-portal.md index a84de16..aeb7639 100644 --- a/docs/roadmap/p02c-4-operator-portal.md +++ b/docs/roadmap/p02c-4-operator-portal.md @@ -1,6 +1,12 @@ # P02c-4: Operator Portal MVP -> **Status: ⏳ Not started.** Depends on [P02c-1](p02c-1-hub-domain-core.md) and [P02c-2](p02c-2-internal-api-and-contract.md). Runs in parallel with [P02c-3](p02c-3-learnstack-integration.md) — this packet touches no LearnStack code. +> **Status: ⏳ Not started.** Depends on [P02c-1](p02c-1-hub-domain-core.md) and [P02c-2](p02c-2-internal-api-and-contract.md). Runs in parallel with [P02c-3](p02c-3-learnstack-integration.md). **Cross-repo, in one +> narrow place:** the operator roles and the MFA-required browser flow are added to the +> single owning realm export, which lives in the LearnStack repository at +> `infra/keycloak/realms/learnstack-hub.json` because LearnStack's compose imports both +> realms at first boot. That is a coordinated pull request per +> [Coordination protocol](README.md#coordination-protocol); everything else in this +> packet is Hub-only. ## Goal From 6adf93638e76114eda9c18a5095d0fe3c0fd43b1 Mon Sep 17 00:00:00 2001 From: Cemil ILIK Date: Mon, 10 Aug 2026 21:24:24 +0300 Subject: [PATCH 3/3] docs: update SKILL and README files for clarity on cross-repo coordination and .NET SDK usage --- .claude/skills/commit-and-pr/SKILL.md | 9 ++++++++- .claude/skills/start-task/SKILL.md | 5 +++-- CONTRIBUTING.md | 21 ++++++++++++++------- README.md | 8 ++++++-- infra/compose/README.md | 16 +++++++++++++--- 5 files changed, 44 insertions(+), 15 deletions(-) diff --git a/.claude/skills/commit-and-pr/SKILL.md b/.claude/skills/commit-and-pr/SKILL.md index a39a8c7..fbe4b03 100644 --- a/.claude/skills/commit-and-pr/SKILL.md +++ b/.claude/skills/commit-and-pr/SKILL.md @@ -63,7 +63,14 @@ Feature branch `feat/phase-02c-packet-N-`. Push with `-u` only when the us ### Step 5 — Cross-repo coordination (if the packet spans both repos) -Per [CLAUDE.md § Cross-repo coordination](../../../CLAUDE.md): the Hub-side PR opens first (it carries the canonical contract shape); the LearnStack-side PR references the Hub PR's commit hash; both merge in the same session. Adding/changing a contract endpoint requires a new ADR in the `HodeTech/LearnStack` repo under `docs/decisions/` first, merged ahead of both code PRs. **Do not** push or merge anything in `../LearnStack` without explicit user permission — another agent may be active there. +Per [CLAUDE.md § Cross-repo coordination](../../../CLAUDE.md), in this order — the sequence is the point, not the list of artefacts: + +1. **Merge the LearnStack ADR PR first**, if the packet adds or reshapes a contract endpoint. The ADR lands in the `HodeTech/LearnStack` repo under `docs/decisions/` and is **merged** — not merely opened — before either code PR exists. Code written against an unmerged ADR is code written against a proposal. +2. **Open the Hub code PR.** It carries the canonical contract shape; that is why it goes first. +3. **Open the LearnStack code PR**, referencing the Hub PR's commit hash, written against the shape step 2 fixed. +4. **Merge the two code PRs together**, in the same session. Either-side merge alone leaves the contract dangling. + +**Do not** push or merge anything in `../LearnStack` without explicit user permission — including the ADR PR in step 1 — because another agent may be active there. ## Validation diff --git a/.claude/skills/start-task/SKILL.md b/.claude/skills/start-task/SKILL.md index 2405fab..aa24c58 100644 --- a/.claude/skills/start-task/SKILL.md +++ b/.claude/skills/start-task/SKILL.md @@ -43,8 +43,9 @@ Read, don't skim. If `git log` shows recent edits to the surface, read the commi ### Step 2 — Check packet fit -**The Hub track is frozen from P02c-2 onward** (owner decision 2026-08-08). Before -anything else, check the task against [CLAUDE.md § What state this is in](../../../CLAUDE.md) +**The Hub track is frozen from P02c-2 onward** (owner decision 2026-08-08). At the start of +Step 2 — after Step 1's reading, which is what tells you which packet the task belongs to — +check the task against [CLAUDE.md § What state this is in](../../../CLAUDE.md) and [the freeze](../../../docs/roadmap/README.md): if it belongs to P02c-2 or any packet after it, **say so and stop** — the two conditions that resume the track are named there, and neither has fired. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b0d5646..9e26d22 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,13 +4,20 @@ LearnStack Hub follows the same engineering rigour as [LearnStack core](https:// ## Branch protection -Required status checks on `main`: - -- `backend` — `dotnet build` + format verify + unit + architecture + contract tests -- `frontend` — pnpm install + typecheck + lint + build + Vitest -- `meta` — Markdown link audit on changed docs. Backend format verification runs in - `backend` (`dotnet format --verify-no-changes`); frontend lint runs in `frontend`. -- `secret-scan` — Leakwatch scan (gates per LearnStack Standards 12 § Secrets Management) +Required status checks on `main`. These are the names GitHub **emits** — a job's `name:` +in [`ci.yml`](.github/workflows/ci.yml), not its job id — so branch protection must be +configured on the left-hand strings exactly. A rule configured on the bare job id (`meta`, +`backend`) never matches and silently protects nothing: + +- `backend (build + unit + arch + contract)` — `dotnet build` + format verify + unit + architecture + contract tests +- `frontend (typecheck + lint + build + test)` — pnpm install + typecheck + lint + build + Vitest +- `meta (markdown link audit)` — Markdown link audit on changed docs. Backend format + verification runs in the `backend` job (`dotnet format --verify-no-changes`); frontend + lint runs in the `frontend` job. +- `secret scan (leakwatch)` — Leakwatch scan (gates per LearnStack Standards 12 § Secrets Management) + +If a job's `name:` changes in `ci.yml`, the branch-protection entry and this list change +with it, in the same pull request. `backend-integration` runs from **P02c-1**, which landed the first Testcontainers-backed tests (the entitlement-rebuild round trip). It was gated `if: false` from P02c-0 until then. diff --git a/README.md b/README.md index 2161d8f..5c9a610 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,12 @@ cd ../LearnStack-Hub make dev # 3. LearnStack core API +# Both projects pin .NET 10 in global.json, and the system `dotnet` on this +# workstation is .NET 9 — so invoke the SDK explicitly. `export PATH="$HOME/.dotnet:$PATH"` +# once per shell works too; the explicit path is used here so a copy-pasted +# line works on its own. cd ../LearnStack/backend -dotnet run --project src/LearnStack.Api +~/.dotnet/dotnet run --project src/LearnStack.Api # 4. Hub API # The host does not load .env itself, so export it first — otherwise the @@ -65,7 +69,7 @@ cd ../LearnStack-Hub set -a; . ./.env; set +a export POSTGRES_HOST=localhost cd backend -dotnet run --project src/Core/LearnStack.Hub.Api +~/.dotnet/dotnet run --project src/Core/LearnStack.Hub.Api ``` In production, the two repos deploy independently — the shared compose is a dev-time convenience only. diff --git a/infra/compose/README.md b/infra/compose/README.md index a0483f2..86fa412 100644 --- a/infra/compose/README.md +++ b/infra/compose/README.md @@ -21,12 +21,22 @@ cd ../LearnStack && make dev cd ../LearnStack-Hub && make dev # 3. LearnStack core API -cd ../LearnStack/backend && dotnet run --project src/LearnStack.Api +# `~/.dotnet/dotnet`, not plain `dotnet`: both projects pin .NET 10 in +# global.json and the system dotnet on this workstation is .NET 9. +cd ../LearnStack/backend && ~/.dotnet/dotnet run --project src/LearnStack.Api -# 4. Hub API -cd ../LearnStack-Hub/backend && dotnet run --project src/Core/LearnStack.Hub.Api +# 4. Hub API — see the note below; this line is not the whole step. +cd ../LearnStack-Hub/backend && ~/.dotnet/dotnet run --project src/Core/LearnStack.Hub.Api ``` +**Step 4 needs `.env` exported into the shell first.** The Hub host does not load `.env` +itself, so without it the connection string falls back to defaults and the API cannot +reach Postgres. The full step — the `set -a; . ./.env; set +a` sequence and the +`POSTGRES_HOST=localhost` override — is written once, in +[the repository README's Dev Workflow](../../README.md#dev-workflow). Follow it there +rather than reconstructing it here; a second copy is what let this file drift out of step +in the first place. + The boot-order dependency is dev-only. Production deploys the two stacks independently. ## Health verification