Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .claude/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Reusable, task-focused instruction packs ("skills") for AI coding agents working on **LearnStack Hub**. Each subdirectory is one skill; its `SKILL.md` carries YAML frontmatter (`name`, `description`) so the agent runtime can pick or skip it without reading the whole body.

Skills are **project-local** to this repo (`.claude/skills/`). An agent running from the `learnstack-hub` root loads them automatically. They cite LearnStack core's standards / ADRs by sibling path (`../LearnStack/docs/...`) for cross-cutting authority and carry only the **Hub-specific delta** on top — they never duplicate the LearnStack standards corpus.
Skills are **project-local** to this repo (`.claude/skills/`). An agent running from the `LearnStack-Hub` root loads them automatically. They cite LearnStack core's standards / ADRs by absolute GitHub URL (`https://github.com/HodeTech/LearnStack/blob/main/docs/...`) for cross-cutting authority and carry only the **Hub-specific delta** on top — they never duplicate the LearnStack standards corpus. Sibling-relative `../LearnStack/...` paths remain correct for shell commands and filesystem locations, never for a Markdown link.

## The Hub deltas every skill assumes

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/add-hub-module/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ description: >

## Purpose

Stand up a new Hub modular-monolith module that complies with the dependency-direction rules from day one: four packages, the right references, a registration extension, a DbContext in the `hub` schema (no RLS), and architecture-test coverage. Mirrors LearnStack's [add-backend-module](https://github.com/HodeTech/LearnStack/blob/main/.claude/skills/add-backend-module/SKILL.md) minus the tenant-isolation layer.
Stand up a new Hub modular-monolith module that complies with the dependency-direction rules from day one: four packages, the right references, a registration extension, a DbContext in the `hub` schema (no RLS), and architecture-test coverage. The module pattern is defined in this repository by [module-topology.md](../../../docs/architecture/module-topology.md) and the four module directories already on `main` under `backend/src/Modules/`; copy the shape from those, not from a sibling-repo skill. It is LearnStack core's four-package layout **minus the entire tenant-isolation layer** — no RLS, no `[TenantOwned]`, no query filter, no `ITenantContext`.

## When to use

Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/commit-and-pr/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ Conventional Commits `type(scope): subject`:
- **Hub scopes:** `hub` (cross-cutting), `hub-domain` (aggregates / modules), `hub-infra` (compose / APISIX / Dapr / Vault / EF), `hub-portal` (operator portal), `hub-docs` (documentation).
- **subject:** imperative, ≤ 72 chars.
- **body:** one short paragraph saying _why_ (the diff is _what_). For a packet, name the packet (`P02c-1`).
- **trailer:** `Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>` (or `Codex …` for Codex sessions). HEREDOC every multi-line message.
- **trailer:** one `Co-Authored-By:` line per agent that **materially contributed**, each naming that agent's own identity — never a hard-coded string for a session some other agent ran. [CLAUDE.md § Commit conventions](../../../CLAUDE.md#commit-conventions) carries the per-runtime strings; LearnStack's [Git Workflow Standards § Trailers](https://github.com/HodeTech/LearnStack/blob/main/docs/standards/14-git-workflow.md#trailers) is the authority if the two disagree. HEREDOC every multi-line message.

```bash
git commit -m "$(cat <<'EOF'
feat(hub-domain): P02c-1 — LearnStackTenant aggregate + DbContext + migration

<why, 1-3 sentences>

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: <this session's agent identity>
EOF
)"
```
Expand Down
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ HUB_DAPR_SIDECAR_GRPC_PORT=50002

# ─── Hub API host (LearnStack.Hub.Api, runs on workstation via `dotnet run`) ─
ASPNETCORE_ENVIRONMENT=Development
# The composition root reads DeploymentMode exactly once and fails closed:
# it may be omitted only when ASPNETCORE_ENVIRONMENT=Development, and an
# unrecognised value is rejected rather than coerced. Values (ADR-0020):
# Development | SaaS | Dedicated | SelfHostedOnline | SelfHostedAirGapped.
Hub__DeploymentMode=Development
# Different from LearnStack.Api's 5080 so both can run side-by-side.
#
# Bind to 0.0.0.0 (not localhost / 127.0.0.1) so containers can reach the
Expand Down
3 changes: 2 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

<!--
What does this PR change? Link the relevant Phase 02c packet
(P02c-0..P02c-7) or LearnStack ADR (../LearnStack/docs/decisions/NNNN-*).
(P02c-0..P02c-7) or LearnStack ADR — by absolute URL,
https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/NNNN-*.md.
For cross-repo work, link the paired LearnStack core PR.
-->

Expand Down
11 changes: 6 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,12 @@ jobs:

- name: Markdown link audit (changed docs)
# Mirror of LearnStack core's link audit. Skips externals + anchors.
# Hub's docs cross-link to LearnStack core via `../LearnStack/...`
# relative paths — those links resolve only on the developer's
# workstation (sibling layout). CI can't validate them because the
# LearnStack core repo isn't checked out alongside; the audit skips
# `../LearnStack/` paths explicitly.
# Hub's docs cross-link to LearnStack core by ABSOLUTE GitHub URL,
# which the external-URL skip already covers. A sibling-relative
# `../LearnStack/...` documentation link is **rejected**, not skipped:
# it 404s on github.com and depends on a local checkout. Sibling paths
# are reserved for shell commands and filesystem locations, which are
# not Markdown links and never reach this audit.
env:
EVENT_NAME: ${{ github.event_name }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref }}
Expand Down
25 changes: 2 additions & 23 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,29 +6,8 @@

## Differences from CLAUDE.md

There are no rule differences. The only thing that varies between agent runtimes is the **`Co-Authored-By` commit trailer**, which names the assistant that contributed:

- Claude Code sessions:
`Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>`
- OpenAI Codex sessions:
`Co-Authored-By: Codex Opus 4.7 (1M context) <noreply@anthropic.com>`

If multiple agents contributed materially to the same commit, include one trailer per agent. [LearnStack's Git Workflow Standards § Trailers](https://github.com/HodeTech/LearnStack/blob/main/docs/standards/14-git-workflow.md#trailers) is the authority for both strings; if this file and that section disagree, that section wins.

## Skills

**This repository maintains its own skill catalogue** at [`.claude/skills/`](.claude/skills/README.md) — 18 skills, git-tracked through an un-ignore rule in `.gitignore`. Load them from here, not from the sibling LearnStack repo.

The Hub catalogue is Hub-tailored: the `add-hub-*` workflows encode Hub's deltas from LearnStack core — no Row Level Security, `OperatorId` rather than `UserId`, a six-step MediatR pipeline rather than eight, the `hub` schema, the `learnstack_hub` database. Skills cite LearnStack's standards and ADRs by absolute URL (`https://github.com/HodeTech/LearnStack/blob/main/docs/...`) for cross-cutting authority and carry only the Hub-specific workflow on top; they do not duplicate the standards.

Entry-point selection is the same as in LearnStack core: [implement-task](.claude/skills/implement-task/SKILL.md) for substantive work, [start-task](.claude/skills/start-task/SKILL.md) for scoping only, [standards-check](.claude/skills/standards-check/SKILL.md) followed by [code-review](.claude/skills/code-review/SKILL.md) for review. Pick exactly one entry point; it dispatches the rest.

## Sibling paths

The two repositories sit side by side on disk as `LearnStack/` and `LearnStack-Hub/`, with those exact capitalisations. That layout governs **shell commands and filesystem paths** — `cd ../LearnStack`. A lower-cased spelling works on macOS's case-insensitive filesystem and fails on Linux CI.

Documentation **links** into LearnStack are absolute URLs: `https://github.com/HodeTech/LearnStack/blob/main/docs/...`. A relative link does not cross a repository boundary on github.com, and the CI link audit no longer exempts cross-repo links — so a relative one now fails the build rather than reaching a reader.
There are no rule differences, and the one thing that varies between agent runtimes — the **`Co-Authored-By` commit trailer**, which names the agent that actually contributed — is defined in [CLAUDE.md § Commit conventions](CLAUDE.md#commit-conventions) along with the per-runtime strings and the one-trailer-per-contributing-agent rule. It is not restated here, because two copies of a string that must match a third ([LearnStack's Git Workflow Standards § Trailers](https://github.com/HodeTech/LearnStack/blob/main/docs/standards/14-git-workflow.md#trailers), which is the authority) is how the copies drift.

## Maintaining this file

Do **not** copy CLAUDE.md content into AGENTS.md. If a guidance rule needs to change, change CLAUDE.md; the rule applies everywhere by virtue of the redirect above. The two sections above are the only content that legitimately lives here, because they describe this file's own runtime rather than the project's rules.
Do **not** copy CLAUDE.md content into AGENTS.md. If a guidance rule needs to change, change CLAUDE.md; the rule applies everywhere by virtue of the redirect above. Nothing in this file is a rule of its own — the sections above describe only why this filename exists and where to read instead.
12 changes: 9 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ For any task in this repo, read in this order:

Then pick an entry-point skill from **this repo's** catalogue at [`.claude/skills/`](.claude/skills/README.md). Hub maintains its own Hub-tailored skill set — the `add-hub-*` workflows encode Hub's deltas from LearnStack core (no RLS, `OperatorId` not `UserId`, the 6-step MediatR pipeline, the `hub` schema, the `learnstack_hub` database). The entry point for substantive work is [implement-task](.claude/skills/implement-task/SKILL.md); for scoping-only use [start-task](.claude/skills/start-task/SKILL.md); for review run [standards-check](.claude/skills/standards-check/SKILL.md) then [code-review](.claude/skills/code-review/SKILL.md).

> Hub skills are project-local: an agent running from the `LearnStack-Hub` root loads them from `.claude/skills/`. They cite LearnStack core's standards / ADRs by sibling path (`../LearnStack/docs/...`) for the cross-cutting authority and carry only the Hub-specific workflow on top — they do not duplicate the standards.
> Hub skills are project-local: an agent running from the `LearnStack-Hub` root loads them from `.claude/skills/`. They cite LearnStack core's standards / ADRs by absolute GitHub URL (`https://github.com/HodeTech/LearnStack/blob/main/docs/...`) for the cross-cutting authority and carry only the Hub-specific workflow on top — they do not duplicate the standards.

## Hard rules

Expand Down Expand Up @@ -131,8 +131,14 @@ Hub follows LearnStack's engineering standards by reference unless explicitly ov

- Conventional Commits: `type(scope): subject`. Subject in imperative mood; ≤ 72 chars.
- Hub-specific scopes: `hub`, `hub-portal`, `hub-domain`, `hub-infra`, `hub-docs`.
- Commits made with AI assistance carry the trailer:
`Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>`
- Commits made with AI assistance carry a `Co-Authored-By:` trailer — **one per agent that
materially contributed**, each naming that agent's own runtime-specific identity, never a
fixed string standing in for whichever agent actually ran:
- Claude Code sessions: `Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>`
- OpenAI Codex sessions: `Co-Authored-By: Codex Opus 4.7 (1M context) <noreply@anthropic.com>`

[LearnStack's Git Workflow Standards § Trailers](https://github.com/HodeTech/LearnStack/blob/main/docs/standards/14-git-workflow.md#trailers)
is the authority for the strings; if this list and that section disagree, that section wins.

## Cross-repo coordination

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ LearnStack Hub is **not** an LMS, **not** a tenant-facing surface, and **never**

## Status

**P02c-0 — Repository bootstrap** ✅. Solution scaffold, frontend monorepo, compose stack, CI, and the docs skeleton are in place. No Hub domain code is on `main`.
**P02c-0 — Repository bootstrap** ✅. Solution scaffold, frontend monorepo, compose stack, CI, and the docs skeleton are in place.

**P02c-1 (Hub Domain Core) shipped 2026-08-09.** It was reviewed against the restructured corpus first: although it was written before [ADR-0033](https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/0033-audit-durability-model.md), [ADR-0034](https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/0034-hub-contract-surface-invariant.md) and [ADR-0035](https://github.com/HodeTech/LearnStack/blob/main/docs/decisions/0035-demand-gated-infrastructure.md), it implements none of what they changed — its entitlement wire shape already carries `grace_until` and `generation`, it hosts no endpoint, and its audit behavior is a shell.

Expand All @@ -29,7 +29,7 @@ The architecture deep-dive lives in the sibling repo: [docs/architecture/24-lear

Hub documentation **links** to LearnStack core by absolute URL (`https://github.com/HodeTech/LearnStack/blob/main/docs/...`), per [Documentation Standards § Layout](https://github.com/HodeTech/LearnStack/blob/main/docs/standards/13-documentation.md). A relative path does not cross a repository boundary on github.com and depends on a sibling checkout being present and identically capitalised; an absolute URL works everywhere. Shell commands and filesystem references still use `../LearnStack` — those are paths, not links — so the expected on-disk layout still matters:

```
```text
<parent-dir>/
├── LearnStack/ (https://github.com/HodeTech/LearnStack)
└── LearnStack-Hub/ (this repo)
Expand Down
14 changes: 8 additions & 6 deletions backend/src/Core/LearnStack.Hub.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@
var builder = WebApplication.CreateBuilder(args);

// DeploymentMode is read exactly once, here at the composition root; modules
// never read it (Modules_Do_Not_Reference_DeploymentMode).
var deploymentMode = Enum.TryParse<DeploymentMode>(
// never read it (Modules_Do_Not_Reference_DeploymentMode). The resolution rule
// fails closed and is unit-tested in DeploymentModeResolverTests: only the
// Development environment may leave it unset, and only an exact member name is
// accepted. Silently coercing an unset or unrecognised value to Development
// would hand a production host the development error-tracking and resilience
// providers without a word in the log.
var deploymentMode = DeploymentModeResolver.Resolve(
builder.Configuration["Hub:DeploymentMode"],
ignoreCase: true,
out var parsed)
? parsed
: DeploymentMode.Development;
builder.Environment.IsDevelopment());

builder.AddHubSerilog();
builder.AddHubOpenTelemetry();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ namespace LearnStack.Hub.Application.Pipeline;
/// Hub MediatR pipeline. Hub drops exactly one behavior LearnStack core's seven
/// carry — <c>TenantContextBehavior</c> — because Hub is operator-administered,
/// not tenant-isolated. (ADR-0032 writes core's as an eight-step list because it
/// counts the Handler; this file counts behaviors.) Outermost (validation) first, innermost (handler) last; the
/// <c>MediatR_Pipeline_Order_Matches_Canonical_Sequence</c> architecture test
/// asserts this DI registration order.
/// counts the Handler; this file counts behaviors.) Outermost (validation) first, innermost (handler) last.
/// <c>MediatR_Pipeline_Order_Matches_Canonical_Sequence</c> asserts the declared
/// order in <see cref="CanonicalBehaviorOrder"/>, and
/// <c>AddHubMediatRPipeline_Registers_The_Canonical_Order</c> asserts that
/// <see cref="AddHubMediatRPipeline"/> registers exactly that sequence.
/// </summary>
public static class MediatRPipelineRegistration
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
namespace LearnStack.Hub.SharedKernel.Hosting;

/// <summary>
/// Resolves the configured <see cref="DeploymentMode"/> at the composition root.
/// It is a pure function of the configured string and the host environment so it
/// can be unit-tested; the composition root remains the only place that reads
/// <c>Hub:DeploymentMode</c> from configuration.
/// <para>
/// It <strong>fails closed</strong>. Only the Development environment may leave
/// the value unset, and only an exact (case-insensitive) member name is accepted.
/// <c>Enum.TryParse</c> is deliberately not used: it also accepts numeric text
/// (<c>"3"</c>) and comma-separated lists (<c>"SaaS,Dedicated"</c>, which ORs to
/// <c>3</c>), both of which land on a defined member and would slip past an
/// <c>Enum.IsDefined</c> guard while meaning nothing an operator intended to write.
/// </para>
/// </summary>
public static class DeploymentModeResolver
{
/// <summary>
/// Resolves the mode, or throws <see cref="InvalidOperationException"/> with a
/// message naming the key and its valid values.
/// </summary>
/// <param name="configuredValue">The raw <c>Hub:DeploymentMode</c> value; surrounding whitespace is ignored.</param>
/// <param name="isDevelopmentEnvironment">Whether the host environment is Development.</param>
public static DeploymentMode Resolve(string? configuredValue, bool isDevelopmentEnvironment)
{
var trimmed = configuredValue?.Trim();

if (string.IsNullOrEmpty(trimmed))
{
if (!isDevelopmentEnvironment)
{
throw new InvalidOperationException(
$"Hub:DeploymentMode is not configured. It is required outside the Development environment. {ValidValuesSuffix()}");
}

return DeploymentMode.Development;
}

foreach (var candidate in Enum.GetValues<DeploymentMode>())
{
if (string.Equals(candidate.ToString(), trimmed, StringComparison.OrdinalIgnoreCase))
{
return candidate;
}
}

throw new InvalidOperationException(
$"Hub:DeploymentMode '{trimmed}' is not a valid DeploymentMode. {ValidValuesSuffix()}");
}

private static string ValidValuesSuffix() =>
$"Expected exactly one of: {string.Join(", ", Enum.GetNames<DeploymentMode>())}.";
}
Loading
Loading