Initialize repository with harness code and assets

Initial import of all source code, config, and README assets: the
packages workspace (cli, core, server, web, docs, landing, skills),
build scripts, tooling config, and CI workflows.

Includes the data-layout revision made on this branch: the local data
root defaults to ~/.penguin/data (PENGUIN_HOME still overrides; the
installer keeps its binaries in ~/.penguin), and every Agent lives
under <project>/agents/<agent>/ — path helpers, the three
agent-enumeration scans, the system prompt, built-in Skills, tests
and docs all follow the new layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ihk8iQuo3kv2aPjAYEPuR
This commit is contained in:
Yaowei Zheng
2026-07-19 14:06:53 +08:00
committed by GitHub
parent 056bed7aeb
commit 45bfae6e94
543 changed files with 92949 additions and 0 deletions
@@ -0,0 +1,54 @@
/**
* "Copy Markdown" button: puts the page's Markdown source on the clipboard with a
* transient "copied" state — so a page can be pasted into a model context, an issue
* or a note as clean Markdown rather than rendered HTML.
*/
import { useEffect, useRef, useState } from "react";
import { S } from "../lib/strings";
import { CheckIcon, CopyIcon } from "./icons";
export function CopyMarkdownButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false);
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(
() => () => {
if (timer.current) clearTimeout(timer.current);
},
[],
);
const onCopy = async () => {
try {
await navigator.clipboard.writeText(text);
} catch {
// Clipboard API unavailable (e.g. non-secure context): fall back to a hidden textarea.
const ta = document.createElement("textarea");
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
ta.remove();
}
setCopied(true);
if (timer.current) clearTimeout(timer.current);
timer.current = setTimeout(() => setCopied(false), 1600);
};
return (
<button
type="button"
onClick={() => void onCopy()}
title={copied ? S.doc.copied : S.doc.copyMarkdown}
aria-label={copied ? S.doc.copied : S.doc.copyMarkdown}
className="inline-flex shrink-0 items-center gap-1.5 rounded-md border border-gray-200 bg-white px-2.5 py-1.5 text-xs text-gray-600 transition-colors hover:bg-gray-50 hover:text-gray-900 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-100"
>
{copied ? (
<CheckIcon className="h-3.5 w-3.5 text-green-600 dark:text-green-400" />
) : (
<CopyIcon className="h-3.5 w-3.5" />
)}
<span>{copied ? S.doc.copied : S.doc.copyMarkdown}</span>
</button>
);
}
+25
View File
@@ -0,0 +1,25 @@
/** Slim docs footer: copyright + repo / license / main-site links on one line. */
import { S } from "../lib/strings";
import { LICENSE_URL, REPO_URL, SITE_URL } from "../lib/links";
export function Footer() {
const link = "transition-colors hover:text-gray-900 dark:hover:text-gray-100 whitespace-nowrap";
return (
<footer className="border-t border-gray-200 dark:border-gray-800">
<div className="mx-auto flex max-w-7xl flex-wrap items-center justify-between gap-x-6 gap-y-2 px-4 py-6 text-xs text-gray-400 sm:px-6 dark:text-gray-500">
<p>{S.footer.copyright}</p>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1">
<a href={SITE_URL} className={link}>
{S.footer.site}
</a>
<a href={REPO_URL} target="_blank" rel="noreferrer" className={link}>
{S.footer.repo}
</a>
<a href={LICENSE_URL} target="_blank" rel="noreferrer" className={link}>
{S.footer.license}
</a>
</div>
</div>
</footer>
);
}
+125
View File
@@ -0,0 +1,125 @@
/**
* Inline icon set (lucide-style 24x24 stroke icons + the GitHub mark), the subset of
* the landing page's set that the docs UI needs. Kept local so the docs site has zero
* icon dependencies; all icons inherit currentColor.
*/
import type { ReactNode, SVGProps } from "react";
type IconProps = SVGProps<SVGSVGElement>;
function Icon({ children, ...props }: IconProps & { children: ReactNode }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
{children}
</svg>
);
}
export function GitHubIcon(props: IconProps) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.56 0-.28-.01-1.02-.02-2-3.2.7-3.88-1.54-3.88-1.54-.52-1.33-1.28-1.68-1.28-1.68-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.03 1.76 2.7 1.25 3.36.96.1-.75.4-1.25.72-1.54-2.55-.29-5.24-1.28-5.24-5.68 0-1.26.45-2.28 1.18-3.09-.12-.29-.51-1.46.11-3.05 0 0 .96-.31 3.15 1.18a10.9 10.9 0 0 1 5.74 0c2.18-1.49 3.14-1.18 3.14-1.18.63 1.59.24 2.76.12 3.05.74.81 1.18 1.83 1.18 3.09 0 4.41-2.69 5.38-5.25 5.67.41.35.78 1.05.78 2.12 0 1.53-.02 2.76-.02 3.14 0 .3.21.67.8.55A11.51 11.51 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z" />
</svg>
);
}
export function SunIcon(props: IconProps) {
return (
<Icon {...props}>
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41" />
</Icon>
);
}
export function MoonIcon(props: IconProps) {
return (
<Icon {...props}>
<path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" />
</Icon>
);
}
export function MonitorIcon(props: IconProps) {
return (
<Icon {...props}>
<rect x="2" y="3" width="20" height="14" rx="2" />
<path d="M8 21h8M12 17v4" />
</Icon>
);
}
export function CopyIcon(props: IconProps) {
return (
<Icon {...props}>
<rect x="9" y="9" width="13" height="13" rx="2" />
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
</Icon>
);
}
export function CheckIcon(props: IconProps) {
return (
<Icon {...props}>
<path d="M20 6 9 17l-5-5" />
</Icon>
);
}
export function MenuIcon(props: IconProps) {
return (
<Icon {...props}>
<path d="M4 6h16M4 12h16M4 18h16" />
</Icon>
);
}
export function XIcon(props: IconProps) {
return (
<Icon {...props}>
<path d="M18 6 6 18M6 6l12 12" />
</Icon>
);
}
export function ArrowRightIcon(props: IconProps) {
return (
<Icon {...props}>
<path d="M5 12h14M12 5l7 7-7 7" />
</Icon>
);
}
export function GlobeIcon(props: IconProps) {
return (
<Icon {...props}>
<circle cx="12" cy="12" r="10" />
<path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10Z" />
</Icon>
);
}
export function ChevronDownIcon(props: IconProps) {
return (
<Icon {...props}>
<path d="m6 9 6 6 6-6" />
</Icon>
);
}
export function ExternalLinkIcon(props: IconProps) {
return (
<Icon {...props}>
<path d="M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
</Icon>
);
}
@@ -0,0 +1,74 @@
/**
* Language menu: 中文 / English / follow system, persisted via the locale context.
* A small dropdown (globe + current label); closes on outside click or selection.
* Scroll position across the locale remount is preserved by LocaleScope.
*/
import { useEffect, useRef, useState } from "react";
import { useLocale } from "../state/locale";
import type { LangPref } from "../state/locale";
import { S } from "../lib/strings";
import { CheckIcon, ChevronDownIcon, GlobeIcon } from "./icons";
export function LangToggle() {
const { lang, setLang } = useLocale();
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!open) return;
const onDown = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
};
document.addEventListener("mousedown", onDown);
return () => document.removeEventListener("mousedown", onDown);
}, [open]);
const OPTIONS: Array<{ value: LangPref; label: string }> = [
{ value: "en", label: S.lang.en },
{ value: "zh", label: S.lang.zh },
{ value: "system", label: S.lang.system },
];
const current = OPTIONS.find((o) => o.value === lang) ?? OPTIONS[2]!;
return (
<div ref={ref} className="relative">
<button
type="button"
onClick={() => setOpen((v) => !v)}
title={S.lang.label}
aria-haspopup="menu"
aria-expanded={open}
className="inline-flex h-9 items-center gap-1.5 rounded-lg border border-transparent px-2.5 text-sm text-gray-600 transition-colors hover:border-gray-200 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:border-gray-800 dark:hover:bg-gray-900 dark:hover:text-gray-100"
>
<GlobeIcon className="h-[18px] w-[18px]" />
<span className="hidden sm:inline">{current.label}</span>
<ChevronDownIcon className="h-3.5 w-3.5" />
</button>
{open && (
<div
role="menu"
className="anim-fade absolute right-0 z-40 mt-1 w-36 rounded-lg border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-900"
>
{OPTIONS.map((o) => (
<button
key={o.value}
type="button"
role="menuitemradio"
aria-checked={lang === o.value}
onClick={() => {
setLang(o.value);
setOpen(false);
}}
className="flex w-full items-center justify-between px-3 py-1.5 text-left text-sm text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800"
>
{o.label}
{lang === o.value && (
<CheckIcon className="h-3.5 w-3.5 text-brand-600 dark:text-brand-300" />
)}
</button>
))}
</div>
)}
</div>
);
}
+59
View File
@@ -0,0 +1,59 @@
/**
* Sticky top bar: logo + site name + "Docs" badge, then (right) a link back to the
* main site, GitHub, language/theme toggles and — on small screens — the sidebar
* toggle. The sidebar itself lives in the layout (router.tsx); this bar only flips
* its open state.
*/
import { Link } from "react-router";
import { S } from "../lib/strings";
import { REPO_URL, SITE_URL } from "../lib/links";
import { GitHubIcon, MenuIcon, XIcon } from "./icons";
import { ThemeToggle } from "./theme-toggle";
import { LangToggle } from "./lang-toggle";
export function Nav({ menuOpen, onToggleMenu }: { menuOpen: boolean; onToggleMenu: () => void }) {
return (
<header className="sticky top-0 z-40 border-b border-gray-200 bg-white/85 backdrop-blur dark:border-gray-800 dark:bg-gray-950/85">
<div className="mx-auto flex h-14 max-w-7xl items-center gap-2 px-4 sm:px-6">
<button
type="button"
onClick={onToggleMenu}
aria-label={menuOpen ? S.nav.closeMenu : S.nav.openMenu}
aria-expanded={menuOpen}
className="mr-1 inline-flex h-9 w-9 items-center justify-center rounded-lg border border-transparent text-gray-600 transition-colors hover:border-gray-200 hover:bg-gray-50 lg:hidden dark:text-gray-400 dark:hover:border-gray-800 dark:hover:bg-gray-900"
>
{menuOpen ? <XIcon className="h-5 w-5" /> : <MenuIcon className="h-5 w-5" />}
</button>
<Link to="/" className="flex items-center gap-2">
<img src={`${import.meta.env.BASE_URL}penguin-logo.svg`} alt="" className="h-7 w-7" />
<span className="text-[15px] font-semibold tracking-tight">{S.siteName}</span>
<span className="rounded-full border border-brand-200 bg-brand-50 px-2 py-0.5 text-xs font-medium text-brand-700 dark:border-brand-900 dark:bg-brand-950 dark:text-brand-300">
{S.docsBadge}
</span>
</Link>
<div className="ml-auto flex items-center gap-1">
<a
href={SITE_URL}
className="hidden rounded-md px-2.5 py-1.5 text-sm text-gray-600 transition-colors hover:text-gray-900 sm:inline-block dark:text-gray-400 dark:hover:text-gray-100"
>
{S.nav.home}
</a>
<LangToggle />
<ThemeToggle />
<a
href={REPO_URL}
target="_blank"
rel="noreferrer"
title={S.nav.github}
aria-label={S.nav.github}
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-transparent text-gray-600 transition-colors hover:border-gray-200 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:border-gray-800 dark:hover:bg-gray-900 dark:hover:text-gray-100"
>
<GitHubIcon className="h-[18px] w-[18px]" />
</a>
</div>
</div>
</header>
);
}
+49
View File
@@ -0,0 +1,49 @@
/**
* Docs sidebar: sections + page links from DOCS_NAV, titles resolved from the active
* locale's frontmatter. Desktop: sticky column. Mobile: the layout renders it as an
* overlay panel under the top bar; onNavigate closes that panel.
*/
import { Link, useLocation } from "react-router";
import { S } from "../lib/strings";
import { useLocale } from "../state/locale";
import { DOCS_NAV, HOME_SLUG } from "../lib/nav";
import { docTitle } from "../lib/docs";
export function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
const { locale } = useLocale();
const { pathname } = useLocation();
const activeSlug = pathname.replace(/^\/|\/$/g, "") || HOME_SLUG;
return (
<nav aria-label="Docs" className="text-sm">
{DOCS_NAV.map((section) => (
<div key={section.id} className="mb-6">
<p className="mb-2 text-xs font-semibold tracking-wide text-gray-400 uppercase dark:text-gray-500">
{S.sections[section.id]}
</p>
<ul className="space-y-0.5 border-l border-gray-200 dark:border-gray-800">
{section.slugs.map((slug) => {
const active = slug === activeSlug;
return (
<li key={slug}>
<Link
to={slug === HOME_SLUG ? "/" : `/${slug}`}
onClick={onNavigate}
aria-current={active ? "page" : undefined}
className={`-ml-px block border-l py-1 pl-3 transition-colors ${
active
? "border-brand-600 font-medium text-brand-700 dark:border-brand-400 dark:text-brand-300"
: "border-transparent text-gray-600 hover:border-gray-300 hover:text-gray-900 dark:text-gray-400 dark:hover:border-gray-700 dark:hover:text-gray-100"
}`}
>
{docTitle(slug, locale)}
</Link>
</li>
);
})}
</ul>
</div>
))}
</nav>
);
}
@@ -0,0 +1,24 @@
/** Theme cycle button: light -> dark -> system, icon reflects the current mode. */
import { useTheme } from "../state/theme";
import type { ThemeMode } from "../state/theme";
import { S } from "../lib/strings";
import { MonitorIcon, MoonIcon, SunIcon } from "./icons";
const NEXT: Record<ThemeMode, ThemeMode> = { light: "dark", dark: "system", system: "light" };
export function ThemeToggle() {
const { mode, setMode } = useTheme();
const label = mode === "light" ? S.theme.light : mode === "dark" ? S.theme.dark : S.theme.system;
const IconCmp = mode === "light" ? SunIcon : mode === "dark" ? MoonIcon : MonitorIcon;
return (
<button
type="button"
onClick={() => setMode(NEXT[mode])}
title={`${S.theme.label}: ${label}`}
aria-label={`${S.theme.label}: ${label}`}
className="inline-flex h-9 w-9 items-center justify-center rounded-lg border border-transparent text-gray-600 transition-colors hover:border-gray-200 hover:bg-gray-50 hover:text-gray-900 dark:text-gray-400 dark:hover:border-gray-800 dark:hover:bg-gray-900 dark:hover:text-gray-100"
>
<IconCmp className="h-[18px] w-[18px]" />
</button>
);
}