diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index b8b0f20..6837a5c 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -114,6 +114,13 @@ All design tokens live in `src/app/globals.css` only. Tailwind config references --- +## Ecosystem + +Solon is the governance pillar of a three-product stack (SSOT: `src/lib/config/ecosystem.ts`): +- **OrangeCat** (economy) — orangecat.ch; its allocation policy is governed in Solon, its agent "The Cat" is a voting member +- **FleetCrown** (engineering) — fleetcrown.orangecat.ch; its agent "Loki" is a voting member, its shared deploy workflow ships Solon +- **Solon** (governance) — this repo; `/ecosystem` renders the live governed state + ## Navigation Structure ### Platform diff --git a/README.md b/README.md index ef9abfc..bb3bc51 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,23 @@ Bitcoin-native governance for transparent treasury management and cryptographic --- +## The Stack: Three Pillars + +Solon is the **governance pillar** of a three-product stack, live at [solon.orangecat.ch](https://solon.orangecat.ch): + +| Pillar | Product | Role | +|---|---|---| +| Economy | [OrangeCat](https://orangecat.ch) | Bitcoin-native economic layer — entities, wallets, payments, the public timeline | +| Engineering | [FleetCrown](https://fleetcrown.orangecat.ch) | AI-agent fleet control plane — dispatch, terminals, and the deploy pipeline for the whole stack | +| Governance | **Solon** (this repo) | Proposals, Bitcoin-signed votes, versioned policies, append-only audit | + +The ties are real, not marketing: + +- **OrangeCat's platform allocation policy is governed here.** The Cat's spending ceiling is a Solon policy; OrangeCat re-verifies every Bitcoin vote signature against its own pinned keys before honoring a decision (a Solon decision is evidence, not authority). +- **Both sibling agents are voting members.** The Cat (`orangecat:cat`) and Loki (`fleetcrown:loki`) hold their own keys and cast Bitcoin signed-message votes via `scripts/agent-vote.ts`. Humans-only categories (membership, safety, aid, governance rules) are red lines agents cannot vote on. +- **FleetCrown ships Solon.** `.github/workflows/deploy.yml` calls FleetCrown's shared `selfhost-deploy.yml`; a merge to `main` deploys to production. +- **Decisions are self-verifying.** `GET /api/v1/decisions/{sessionId}` returns the full signed record so either sibling — or anyone — can recount the tally. See `/ecosystem` on the live site for the current governed state. + ## What Solon Does - **Puts public finances on-chain.** Every treasury transaction is tracked against a multi-sig Bitcoin wallet. Amounts stored in satoshis as BigInt — no floating point, no rounding errors, no trust required. diff --git a/src/app/ecosystem/page.tsx b/src/app/ecosystem/page.tsx new file mode 100644 index 0000000..73d5d28 --- /dev/null +++ b/src/app/ecosystem/page.tsx @@ -0,0 +1,294 @@ +import PageLayout from "@/components/ui/page-layout"; +import { prisma } from "@/lib/db"; +import { ECOSYSTEM_PILLARS } from "@/lib/config/ecosystem"; +import { CATEGORY_ELECTORATE } from "@/lib/config/governance"; +import { Electorate, type DecisionCategory } from "@prisma/client"; + +export const dynamic = "force-dynamic"; + +const CATEGORY_LABEL: Record = { + ALLOCATION_POLICY: "Allocation policy", + TREASURY_SPEND: "Treasury spend", + OPERATIONS: "Operations", + AID_DISBURSEMENT: "Aid disbursement", + MEMBERSHIP: "Membership", + SAFETY: "Safety", + GOVERNANCE_RULES: "Governance rules", +}; + +/** + * The three-pillar page: what Solon governs, for whom, and the live proof. + * Everything below the fold is rendered straight from the database — the + * same record the public API serves. If the database is empty, the page + * says so instead of inventing numbers. + */ +export default async function EcosystemPage() { + let org = null; + let members: { + id: string; + displayName: string; + memberType: string; + system: string | null; + bitcoinAddress: string; + status: string; + }[] = []; + let policies: { key: string; version: number; content: unknown }[] = []; + let proposals: { + id: string; + title: string; + category: DecisionCategory; + status: string; + session: { id: string; status: string; outcome: string | null } | null; + }[] = []; + let dbError = false; + + try { + org = await prisma.organization.findFirst({ orderBy: { createdAt: "asc" } }); + if (org) { + [members, policies, proposals] = await Promise.all([ + prisma.member.findMany({ + where: { organizationId: org.id }, + orderBy: { joinedAt: "asc" }, + select: { + id: true, + displayName: true, + memberType: true, + system: true, + bitcoinAddress: true, + status: true, + }, + }), + prisma.policy.findMany({ + where: { organizationId: org.id, status: "ACTIVE" }, + orderBy: { key: "asc" }, + select: { key: true, version: true, content: true }, + }), + prisma.proposal.findMany({ + where: { organizationId: org.id }, + orderBy: { createdAt: "desc" }, + take: 10, + select: { + id: true, + title: true, + category: true, + status: true, + session: { + select: { id: true, status: true, outcome: true }, + }, + }, + }), + ]); + } + } catch { + dbError = true; + } + + const humansOnlyCategories = ( + Object.entries(CATEGORY_ELECTORATE) as [DecisionCategory, Electorate][] + ) + .filter(([, electorate]) => electorate === Electorate.HUMANS_ONLY) + .map(([category]) => category); + + return ( + +
+ {/* The three pillars */} +
+ {ECOSYSTEM_PILLARS.map((pillar) => ( +
+
+

+ {pillar.name} +

+ + {pillar.role} + +
+

+ {pillar.description} +

+

+ {pillar.tie} +

+
+ {pillar.key === "solon" ? ( + + You are here — {pillar.url.replace("https://", "")} + + ) : ( + + {pillar.url.replace("https://", "")} → + + )} +
+
+ ))} +
+ + {/* How a decision travels */} +
+

+ How a decision travels +

+
    + {[ + "A member — human, the Cat, or Loki — files a proposal, signed with their own Bitcoin key. Solon never holds anyone's private key.", + "A voting session opens and snapshots its rules: electorate, threshold, quorum, eligible weight. A past decision stays explainable after the rules change.", + "Members cast Bitcoin signed-message votes from their own environments. One member, one vote per session, enforced by the database.", + "The session closes with an outcome — approved, rejected, or expired — and every step lands in the append-only audit trail.", + "The decision is published as a self-verifying document (/api/v1/decisions/{sessionId}) carrying every signed message, so anyone can recount the tally.", + "OrangeCat and FleetCrown are notified — and OrangeCat re-verifies every vote signature against its own pinned keys before acting. A decision is evidence, not authority.", + ].map((step, i) => ( +
  1. + + {i + 1} + + {step} +
  2. + ))} +
+

+ Red lines: {humansOnlyCategories.map((c) => CATEGORY_LABEL[c].toLowerCase()).join(", ")}{" "} + are decided by humans only. Agents propose anywhere, but can never vote to expand their + own suffrage. +

+
+ + {/* Live governed state */} +
+

+ What is governed here, live +

+ {dbError && ( +

+ The governance register is currently unreachable, so no live state can be shown. +

+ )} + {!dbError && !org && ( +

+ No organization is registered yet — there is nothing governed to show. +

+ )} + {org && ( + <> +

+ Everything below is read from the same database the public API serves — nothing is + staged. +

+ +
+

+ {org.name} — members +

+
    + {members.map((m) => ( +
  • +
    + {m.displayName} + + {m.memberType} + {m.system ? ` · ${m.system}` : ""} · {m.status} + +
    +
    + {m.bitcoinAddress} +
    +
  • + ))} +
+
+ +
+

Active policies

+ {policies.length === 0 ? ( +

No active policies.

+ ) : ( +
    + {policies.map((p) => ( +
  • +
    + + {p.key} + + v{p.version} +
    +
    +                          {JSON.stringify(p.content, null, 2)}
    +                        
    +
  • + ))} +
+ )} +
+ +
+

Recent proposals

+ {proposals.length === 0 ? ( +

No proposals yet.

+ ) : ( +
    + {proposals.map((p) => ( +
  • +
    + {p.title} + + {CATEGORY_LABEL[p.category]} ·{" "} + {p.session?.outcome ?? p.session?.status ?? p.status} + +
    + {p.session?.outcome && ( + + self-verifying decision document → + + )} +
  • + ))} +
+ )} +
+ + )} +
+
+
+ ); +} diff --git a/src/components/ui/footer.tsx b/src/components/ui/footer.tsx index ebbefee..b721b8d 100644 --- a/src/components/ui/footer.tsx +++ b/src/components/ui/footer.tsx @@ -1,6 +1,13 @@ import Link from 'next/link'; +import { + ECOSYSTEM_PILLARS, + SOLON_GITHUB_URL, +} from '@/lib/config/ecosystem'; +// Only routes that actually exist belong here — a footer link to a 404 is a lie. export default function Footer() { + const siblings = ECOSYSTEM_PILLARS.filter((p) => p.key !== 'solon'); + return ( ); } - - - - diff --git a/src/lib/config/ecosystem.ts b/src/lib/config/ecosystem.ts new file mode 100644 index 0000000..dd35f9f --- /dev/null +++ b/src/lib/config/ecosystem.ts @@ -0,0 +1,56 @@ +/** + * SSOT for the three-pillar stack Solon belongs to. + * + * OrangeCat is the economic pillar, FleetCrown the engineering pillar, Solon + * the governance pillar. Every claim in `tie` is verifiable against running + * systems — the live org in this database, the sibling products' public + * sites, or this repo's own deploy workflow — so keep it that way: nothing + * goes in here that a reader cannot check. + */ + +export interface EcosystemPillar { + key: "orangecat" | "fleetcrown" | "solon"; + name: string; + role: string; + url: string; + description: string; + /** The real, checkable relationship between this pillar and Solon. */ + tie: string; +} + +export const ECOSYSTEM_PILLARS: EcosystemPillar[] = [ + { + key: "orangecat", + name: "OrangeCat", + role: "Economy", + url: "https://orangecat.ch", + description: + "Bitcoin-native economic layer: actor-owned entities, BTC/Lightning wallets and payments, the public timeline, and My Cat — an advisory economic AI.", + tie: + "OrangeCat's platform allocation policy is governed in Solon: the Cat's spending ceiling is a Solon policy, the Cat itself is a registered voting member, and OrangeCat independently re-verifies every Bitcoin vote signature before honoring a decision — Solon's word is evidence, not authority.", + }, + { + key: "fleetcrown", + name: "FleetCrown", + role: "Engineering", + url: "https://fleetcrown.orangecat.ch", + description: + "Control plane for running AI-agent fleets across projects: dispatch, live terminals, orchestration, and the deploy pipeline for the whole stack.", + tie: + "FleetCrown's agent Loki is a registered voting member here, casting Bitcoin-signed votes from FleetCrown's own environment — and FleetCrown's shared deploy workflow is what ships Solon itself to production.", + }, + { + key: "solon", + name: "Solon", + role: "Governance", + url: "https://solon.orangecat.ch", + description: + "Bitcoin-native governance: proposals, cryptographically signed votes, versioned policies, and an append-only audit trail.", + tie: + "Solon is where the stack decides. It holds no private keys and no funds — members (human or agent) sign votes with their own Bitcoin keys, and every decision is published as a self-verifying document anyone can recheck.", + }, +]; + +export const SOLON_GITHUB_URL = "https://github.com/maonakamoto/solon"; +export const ORANGECAT_GITHUB_URL = "https://github.com/maonakamoto/orangecat"; +export const FLEETCROWN_GITHUB_URL = "https://github.com/maonakamoto/fleetcrown"; diff --git a/src/lib/site-config.ts b/src/lib/site-config.ts index 568f32a..54b8978 100644 --- a/src/lib/site-config.ts +++ b/src/lib/site-config.ts @@ -31,6 +31,14 @@ export const NAV_ITEMS: NavSection[] = [ title: 'Treasury', children: [{ title: 'Bitcoin Treasury', href: '/treasury/bitcoin' }], }, + { + title: 'Ecosystem', + children: [ + { title: 'Three Pillars', href: '/ecosystem' }, + { title: 'OrangeCat', href: 'https://orangecat.ch' }, + { title: 'FleetCrown', href: 'https://fleetcrown.orangecat.ch' }, + ], + }, { title: 'Resources', children: [{ title: 'About', href: '/about' }], diff --git a/tests/e2e/header-footer.spec.ts b/tests/e2e/header-footer.spec.ts index 1bbcf7b..cf128ed 100644 --- a/tests/e2e/header-footer.spec.ts +++ b/tests/e2e/header-footer.spec.ts @@ -17,8 +17,8 @@ test.describe('Header & Footer', () => { await expect(siteFooter).toBeVisible(); await expect(siteFooter.locator('div:text-is("Platform")')).toBeVisible(); await expect(siteFooter.locator('div:text-is("Governance")')).toBeVisible(); - await expect(siteFooter.locator('div:text-is("Treasury")')).toBeVisible(); - await expect(siteFooter.locator('div:text-is("Company")')).toBeVisible(); + await expect(siteFooter.locator('div:text-is("Ecosystem")')).toBeVisible(); + await expect(siteFooter.locator('div:text-is("Resources")')).toBeVisible(); }); });