diff --git a/DEVLOG.md b/DEVLOG.md index 5901fe42..2997e500 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -2,6 +2,60 @@ > Append-only session log. Read at session start. Update at session end. +## 2026-06-17 — Public landing domain (forge-pm.dev) + docs wired into /docs; public-readiness audit + +Picked `forge-pm.dev` as the public landing/marketing domain; the app stays on +the personal `forge.axiom-labs.dev` (env-driven, untouched). Swapped the +landing canonical everywhere it defaulted to `forge.axiom-labs.dev`: +`landing/app/layout.tsx` (metadataBase + OG), `sitemap.ts`, `robots.ts`, the +OG-card domain text in `opengraph-image.tsx`, and the README default. The +placeholder docs grid had advertised `docs.forge.dev` — a domain we don't own +— now removed (see below). + +### Docs at forge-pm.dev/docs +Docs are already a full VitePress site (`docs/`, base `/docs/`) that the app +builds via `scripts/build-docs.sh` → `public/docs/`. Rather than build a second +renderer in the landing site, mirrored that into the landing static export: +- New `landing/scripts/build-docs.sh` — builds the SAME `../docs` site and + stages dist → `landing/public/docs/`. POSIX sh; `SKIP_DOCS`/`STAGE_ONLY` + knobs; skips gracefully if `docs/` absent. +- `landing/package.json`: `build` = `./scripts/build-docs.sh && next build`; + added `build:app` (docs-less) + `build:docs`. +- Deleted the placeholder `app/docs/page.tsx` + `components/docs-content.tsx` + (VitePress owns the whole `/docs/` subtree incl. its home page; they'd + collide on `out/docs/index.html`). Nav `Docs → /docs/` was already correct. +- Gitignored `landing/public/docs/` (build artifact). +- No Next rewrite needed (unlike the app server): static hosts auto-serve + `index.html` for `/docs/`, and base `/docs/` makes assets resolve. +Verified `pnpm build` → `out/docs/` carries all 60 pages + assets + sitemap +alongside the landing pages; lint + typecheck green. + +### VitePress config (shared by app + landing builds) +- `srcExclude: ["audits/**","plans/**"]` — keep internal execution plans / + audit notes out of the published site (they were unlinked but reachable). + Fixed the one inbound link (`agents/providers-and-transports.md` → the + runtime-adapter ADR) to a GitHub blob URL. +- `sitemap.hostname = forge-pm.dev` + a `transformItems` that injects the + `/docs/` base (VitePress emits page paths without the site base, so raw + output dropped `/docs/`). Emits `/docs/sitemap.xml` with 60 canonical URLs; + landing `robots.ts` now lists both sitemaps. +- `editLink` branch `master → main` (default branch is main; links 404'd). +- Genericized a sample plugin manifest author email (`ops@axiom-labs.dev` → + `you@example.com`). + +### Public-readiness audit (repo is already PUBLIC) +Swept tracked files for secrets / internal data: +- CRITICAL: an earlier DEVLOG entry (~L8265, 2026-04-19 section) carried a + plaintext owner-login credential (the `ADMIN_PASSWORD` from + `~/docker/forge/.env`). Redacted at HEAD. Because it sits in already-pushed + PUBLIC history, the credential MUST be rotated; an optional history rewrite + (filter-repo + force-push) to purge it is pending decision. +- Redacted the internal LAN IP (`172.16.24.250`) across DEVLOG → + ``. +- Otherwise clean: no tracked `.env`/key files; remaining password-like hits + are CI/dev defaults (`forge-dev`, `forgeminio-dev-password`); the built + public docs contain zero leaks; README + MIT LICENSE present. + ## 2026-06-16 — Agent request run-mode chips and durable issue links Implemented AXI-81's first-class issue-comment Agent Request flow. Comments now @@ -6639,7 +6693,7 @@ real dispatch event doubles as a heartbeat. respond to each `kind` (AGENT_ASSIGNED → claim/start, COMMENT_CREATED → read, etc.). - `Agent.webhookUrl` set to - `http://172.16.24.250:8644/webhooks/forge-dispatch` (Victor) and + `http://<internal-host>:8644/webhooks/forge-dispatch` (Victor) and `:8645/...` (Mizu). `Agent.webhookSecret` set to the per-route HMAC secret returned by the subscribe call. Secrets generated with `openssl rand -hex 32`; intermediate file shredded after the DB @@ -6678,7 +6732,7 @@ real dispatch event doubles as a heartbeat. with a stable jobId for dedupe. - Live verification: assigned AXI-32 to Victor → `AGENT_ASSIGNED` event row written → drain enqueued the delivery → worker POSTed to - `http://172.16.24.250:8644/webhooks/forge-dispatch` → HTTP 202 → + `http://<internal-host>:8644/webhooks/forge-dispatch` → HTTP 202 → `recordAgentReachable` bumped `Agent.lastHeartbeatAt` within 14ms of `deliveredAt`. - **Bonus fix during E2E:** existing agent ids in this DB are not @@ -6708,7 +6762,7 @@ presence fresh. - **Mizu cron** also removed; her webhook adapter is live but she had no `lastHeartbeatAt` to begin with — first real dispatch event intended for her will set both URL trust and presence. -- The webhook URL is currently the LAN IP `172.16.24.250` because +- The webhook URL is currently the LAN IP `<internal-host>` because Forge runs in a Docker container on the same host as Hermes (which runs on the host filesystem). For the cross-host case, swap to a reachable hostname. @@ -6798,7 +6852,7 @@ Currently NOT enabled (no listener on 8644). To switch from the poll-style heartbeat / queue-pull pattern to push-style dispatch: flip `platforms.webhook` on in each profile config, subscribe one route per profile, set the corresponding `Agent.webhookUrl` (e.g. -`http://172.16.24.250:8644/webhooks/forge-dispatch`), and let the +`http://<internal-host>:8644/webhooks/forge-dispatch`), and let the existing Forge `WebhookDelivery` worker POST `AGENT_ASSIGNED` straight to Hermes — the route prompt would render "you have a new assignment: AXI-42" and wake Victor's session. MCP polling for @@ -8262,7 +8316,7 @@ Authelia header-bridge didn't land cleanly (edge middleware was setting response - `POST /api/auth/callback/credentials` with valid creds → `302 /inbox`, `__Secure-authjs.session-token` set. - `GET /inbox` with that cookie → `200`. -- Login: `timothy.b.dixon@gmail.com` / `@Tqbfj0tld!` (from `~/docker/forge/.env`). +- Login verified with the seeded admin credentials from `~/docker/forge/.env` (redacted). ### Ran into @@ -9069,10 +9123,10 @@ Findings + fixes: - **Validation:** (1) gated live vitest `codex-app-server-live.test.ts` (CODEX_LIVE=1) bridges stdio→ws and runs the *shipped* connector through real codex — streams `FORGE_CODEX_OK`. (2) The **deployed Forge container** drove - the bridge→codex round-trip over the network (172.16.24.250:4505) and got + the bridge→codex round-trip over the network (<internal-host>:4505) and got `FORGE_PROD_OK` via both streamed deltas and the fallback. - **Prod wiring:** created Runtime `rt_codex_appserver` (adapterKey - codex-app-server, endpoint `ws://172.16.24.250:4505`) in workspace + codex-app-server, endpoint `ws://<internal-host>:4505`) in workspace cmo6cui6q…, attached the `@codex` agent. resolveRunEngine→RUNS via the attached adapter; getRunsConnectorForAgent→codex-app-server connector. @@ -9183,7 +9237,7 @@ override is authoritative. 3. In Forge → Settings → Runtimes, edit `rt_codex_appserver`: set `workspaceRoot=/work` and pick a sandbox/approval tier (leave full-access/never for parity with today). Endpoint stays - `ws://172.16.24.250:4505`. + `ws://<internal-host>:4505`. Live end-to-end of a sandboxed Codex *turn* still needs the operator to send a chat (auth-walled) — every layer beneath that is proven. @@ -9795,7 +9849,7 @@ src/server/routers/__tests__/workspace-members.test.ts` pass (40); `pnpm vitest run tests/unit/codex-app-server.test.ts src/server/services/__tests__/run-dispatcher.test.ts` pass (22); `git diff --check` clean. LAN preview restarted at -`http://172.16.24.250:3020/w/axiom-labs/chat`. +`http://<internal-host>:3020/w/axiom-labs/chat`. ## 2026-06-11 — AXI-78 Codex RUNS split-worker fix diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 36ac8a09..74fa25eb 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -18,6 +18,19 @@ export default defineConfig({ // the last-updated stamp anyway. lastUpdated: false, appearance: true, // light is default; users can flip to dark + // Exclude internal working docs (execution plans + audit notes) from the + // published site. They're in no sidebar and reference internal infra — + // not user documentation. Applies to both the app and public docs builds. + srcExclude: ["audits/**", "plans/**"], + // Emit /docs/sitemap.xml with canonical public URLs (the public docs home, + // not the personal app instance) so every doc page is individually indexable. + // VitePress emits page paths without the site `base`, so inject /docs/ to + // match where the docs are actually served (forge-pm.dev/docs/...). + sitemap: { + hostname: "https://forge-pm.dev", + transformItems: (items) => + items.map((item) => ({ ...item, url: `docs/${item.url.replace(/^\/+/, "")}` })), + }, ignoreDeadLinks: [ /^https?:\/\/localhost/, /^\/api\//, @@ -199,10 +212,9 @@ export default defineConfig({ }, }, - // Edit-link is wired but the repo isn't public yet. Flip the URL - // when the repo flips public. + // Repo is public; "Edit this page" targets the default branch (main). editLink: { - pattern: "https://github.com/Codename-11/forge/edit/master/docs/:path", + pattern: "https://github.com/Codename-11/forge/edit/main/docs/:path", text: "Edit this page on GitHub", }, diff --git a/docs/agents/providers-and-transports.md b/docs/agents/providers-and-transports.md index 2e39845b..c7a3691e 100644 --- a/docs/agents/providers-and-transports.md +++ b/docs/agents/providers-and-transports.md @@ -146,4 +146,5 @@ configured but won't dial: dispatch skips it and chat reports `PLANNED_ADAPTERS`). - Engine resolution + connectors: `src/server/services/dispatch/`. - Design history + remaining work: - [runtime-adapter ADR](../plans/runtime-adapter-refactor.md). + [runtime-adapter ADR](https://github.com/Codename-11/forge/blob/main/docs/plans/runtime-adapter-refactor.md) + (in the repo). diff --git a/docs/automation/plugins.md b/docs/automation/plugins.md index 12a60261..cee482c4 100644 --- a/docs/automation/plugins.md +++ b/docs/automation/plugins.md @@ -15,7 +15,7 @@ the scopes line up. "name": "My Plugin", "version": "0.1.0", "description": "Reacts to issues and suggests labels.", - "author": { "name": "Axiom Labs", "email": "ops@axiom-labs.dev" }, + "author": { "name": "Your Name", "email": "you@example.com" }, "scopes": ["READ_ISSUES", "WRITE_ISSUES"], "events": ["ISSUE_CREATED"], "skills": [ diff --git a/landing/.gitignore b/landing/.gitignore index 8e0fd70b..db8d3a33 100644 --- a/landing/.gitignore +++ b/landing/.gitignore @@ -6,6 +6,9 @@ /out/ next-env.d.ts +# VitePress docs staged in by scripts/build-docs.sh (built artifact) +/public/docs/ + # misc .DS_Store *.tsbuildinfo diff --git a/landing/README.md b/landing/README.md index 2839ec4b..d434abc6 100644 --- a/landing/README.md +++ b/landing/README.md @@ -30,17 +30,27 @@ pnpm dev # binds 0.0.0.0:3200 — http://localhost:3200 (and over t ## Build & deploy ```bash -pnpm build # static export → ./out +pnpm build # build the VitePress docs + static export → ./out +pnpm build:app # landing pages only (skip the docs build) pnpm serve # preview the exported site locally (npx serve) ``` -`out/` is a plain static bundle. Drop it on GitHub Pages, S3/CloudFront, -Netlify, Vercel, or behind the same reverse proxy as the app. No Node server -is required. (If you later need server features, remove `output: "export"` -in `next.config.ts` and run `pnpm start`.) +`pnpm build` runs `scripts/build-docs.sh` first: it builds the shared +VitePress docs (`../docs` — the same site the app serves) into +`public/docs/`, then `next build` exports everything to `out/`, so the full +documentation ships at `/docs/`. For a fast landing-only loop use +`pnpm build:app` (or `SKIP_DOCS=1 pnpm build`). + +`out/` is a plain static bundle. Drop it on any static host — GitHub Pages, +S3/CloudFront, Netlify, Vercel, or behind the same reverse proxy as the app. +No Node server is required to *serve* it; the *build* needs Node + pnpm (the +docs step does a one-time `pnpm --dir ../docs install`). On a host's static +buildpack, set the **build command** to `pnpm build` and the **publish +directory** to `out`. (If you later need server features, remove +`output: "export"` in `next.config.ts` and run `pnpm start`.) Set the canonical URL via `NEXT_PUBLIC_SITE_URL` at build time (used for -metadata / Open Graph). Defaults to `https://forge.axiom-labs.dev`. +metadata / Open Graph). Defaults to `https://forge-pm.dev`. ## Live data diff --git a/landing/app/docs/page.tsx b/landing/app/docs/page.tsx deleted file mode 100644 index 95347acb..00000000 --- a/landing/app/docs/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type { Metadata } from "next"; -import { LandingNav } from "@/components/nav"; -import { LandingFooter } from "@/components/footer"; -import { DocsContent } from "@/components/docs-content"; - -const DESCRIPTION = - "Everything you need to install Forge, connect an agent, and ship the first plan. Get started, runtimes & agents, and full reference."; - -export const metadata: Metadata = { - title: "Docs", - description: DESCRIPTION, - alternates: { canonical: "/docs/" }, - openGraph: { - type: "website", - siteName: "Forge", - url: "/docs/", - title: "Docs · Forge", - description: DESCRIPTION, - images: [{ url: "/opengraph-image", width: 1200, height: 630, alt: "Forge — issue tracking for humans & agents." }], - }, -}; - -// Docs landing — ported from the prototype's DocsPage. -export default function DocsPage() { - return ( -
- -
- -
- -
- ); -} diff --git a/landing/app/layout.tsx b/landing/app/layout.tsx index 2ce4c439..fc29f9e0 100644 --- a/landing/app/layout.tsx +++ b/landing/app/layout.tsx @@ -17,7 +17,7 @@ const jetbrainsMono = JetBrains_Mono({ display: "swap", }); -const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://forge.axiom-labs.dev"; +const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://forge-pm.dev"; const TITLE = "Forge — issue tracking for humans & agents"; const DESCRIPTION = "An open-source work tracker built for the handoff between humans and agents. Humans file, agents pick up, humans review — same issues, same plans, same keyboard. Self-host in 60 seconds."; diff --git a/landing/app/opengraph-image.tsx b/landing/app/opengraph-image.tsx index 8e6e651b..25833f75 100644 --- a/landing/app/opengraph-image.tsx +++ b/landing/app/opengraph-image.tsx @@ -81,7 +81,7 @@ export default async function OgImage() { fontFamily: "ui-monospace, Menlo, monospace", }} > - forge.axiom-labs.dev + forge-pm.dev github.com/Codename-11/forge diff --git a/landing/app/robots.ts b/landing/app/robots.ts index 658872f5..2f44d8b5 100644 --- a/landing/app/robots.ts +++ b/landing/app/robots.ts @@ -2,11 +2,11 @@ import type { MetadataRoute } from "next"; export const dynamic = "force-static"; -const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://forge.axiom-labs.dev"; +const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://forge-pm.dev"; export default function robots(): MetadataRoute.Robots { return { rules: { userAgent: "*", allow: "/" }, - sitemap: `${SITE_URL}/sitemap.xml`, + sitemap: [`${SITE_URL}/sitemap.xml`, `${SITE_URL}/docs/sitemap.xml`], }; } diff --git a/landing/app/sitemap.ts b/landing/app/sitemap.ts index db52194e..432c3f0d 100644 --- a/landing/app/sitemap.ts +++ b/landing/app/sitemap.ts @@ -2,7 +2,7 @@ import type { MetadataRoute } from "next"; export const dynamic = "force-static"; -const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://forge.axiom-labs.dev"; +const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL ?? "https://forge-pm.dev"; export default function sitemap(): MetadataRoute.Sitemap { // Trailing slashes match the exported canonical routes (trailingSlash: true). diff --git a/landing/components/docs-content.tsx b/landing/components/docs-content.tsx deleted file mode 100644 index 42c5c46a..00000000 --- a/landing/components/docs-content.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { Icon } from "@/components/icon"; -import { SectionShell } from "@/components/primitives"; - -type DocItem = { name: string; path: string; blurb: string }; -type DocGroup = { title: string; icon: string; items: DocItem[] }; - -export function DocsContent() { - const groups: DocGroup[] = [ - { - title: "Get started", - icon: "Rocket", - items: [ - { name: "Install Forge", path: "docs.forge.dev/install", blurb: "Postgres, Node, 60 seconds." }, - { name: "Your first issue", path: "docs.forge.dev/first-issue", blurb: "C anywhere. Title. Save." }, - { name: "Connect an agent", path: "docs.forge.dev/connect-agent", blurb: "Hermes, Orca, Claude, Codex." }, - { name: "Self-host with Docker", path: "docs.forge.dev/docker", blurb: "Compose file + env vars." }, - ], - }, - { - title: "Runtimes & agents", - icon: "Bot", - items: [ - { name: "Hermes setup", path: "docs.forge.dev/hermes", blurb: "Persistent gateway." }, - { name: "Orca ADE integration", path: "docs.forge.dev/orca", blurb: "Auto-open + dispatch." }, - { name: "Claude Code (MCP)", path: "docs.forge.dev/claude-code", blurb: "Drop-in config." }, - { name: "Codex CLI (MCP)", path: "docs.forge.dev/codex", blurb: "Streamable HTTP." }, - { name: "Custom HTTP webhook", path: "docs.forge.dev/custom-http", blurb: "BYO bridge." }, - { name: "Dispatch rules", path: "docs.forge.dev/dispatch", blurb: "Capability matching." }, - ], - }, - { - title: "Reference", - icon: "BookOpen", - items: [ - { name: "tRPC API", path: "docs.forge.dev/api", blurb: "Every router + procedure." }, - { name: "MCP tool catalogue", path: "docs.forge.dev/mcp", blurb: "14 tools, all keyboard-driven." }, - { name: "Webhook payloads", path: "docs.forge.dev/webhooks", blurb: "Signing, retries, headers." }, - { name: "Prisma schema", path: "docs.forge.dev/schema", blurb: "The source of truth." }, - { name: "CLI · forge daemon", path: "docs.forge.dev/cli", blurb: "Local runtime." }, - { name: "Keyboard shortcuts", path: "docs.forge.dev/shortcuts", blurb: "Cheat sheet." }, - ], - }, - ]; - return ( - - {/* Search bar */} -
- - Search the docs… - ⌘ K -
- -
- {groups.map((g, i) => ( -
-
- - - -

{g.title}

-
-
- {g.items.map((it, j) => ( - - - {it.name} - - - {it.path} - {it.blurb} - - ))} -
-
- ))} -
-
- ); -} diff --git a/landing/package.json b/landing/package.json index afa5ed85..e7c780fe 100644 --- a/landing/package.json +++ b/landing/package.json @@ -5,7 +5,9 @@ "description": "Forge — public landing site. Standalone Next.js app (static export), decoupled from the main Forge application.", "scripts": { "dev": "next dev --turbo --hostname 0.0.0.0 --port 3200", - "build": "next build", + "build": "./scripts/build-docs.sh && next build", + "build:app": "next build", + "build:docs": "./scripts/build-docs.sh", "start": "next start --hostname 0.0.0.0 --port 3200", "serve": "npx serve out -l tcp://0.0.0.0:3200", "lint": "next lint", diff --git a/landing/scripts/build-docs.sh b/landing/scripts/build-docs.sh new file mode 100755 index 00000000..bee6c693 --- /dev/null +++ b/landing/scripts/build-docs.sh @@ -0,0 +1,61 @@ +#!/bin/sh +# Build the VitePress docs and stage them under landing/public/docs/ so the +# landing site's static export serves the full documentation at +# forge-pm.dev/docs/. +# +# This mirrors the app's ../scripts/build-docs.sh — the SAME VitePress site +# (/docs, base "/docs/") — retargeted into the public marketing site +# instead of the app shell. There is one docs source; both the app +# (forge.axiom-labs.dev/docs/) and the public site (forge-pm.dev/docs/) +# build it. No second renderer. +# +# Because the landing is a pure static export, no Next rewrite is needed: +# static hosts auto-serve index.html for the bare /docs/ directory, and +# VitePress's base "/docs/" makes every asset URL resolve under the subpath. +# +# POSIX sh (the node:20-alpine runner has no bash). STAGE_ONLY=1 re-copies an +# existing dist without rebuilding; SKIP_DOCS=1 skips entirely for a fast +# landing-only inner loop. + +set -eu + +if [ "${SKIP_DOCS:-0}" = "1" ]; then + echo "[landing build-docs] SKIP_DOCS=1 — skipping docs build." + exit 0 +fi + +LANDING_DIR="$(cd "$(dirname "$0")/.." && pwd)" +REPO_DIR="$(cd "$LANDING_DIR/.." && pwd)" +DOCS_DIR="$REPO_DIR/docs" +DIST_DIR="$DOCS_DIR/.vitepress/dist" +PUBLIC_DOCS="$LANDING_DIR/public/docs" + +if [ ! -d "$DOCS_DIR" ]; then + echo "[landing build-docs] No docs/ at $DOCS_DIR — skipping (landing built in isolation)." + exit 0 +fi + +if [ "${STAGE_ONLY:-0}" != "1" ]; then + if [ ! -d "$DOCS_DIR/node_modules" ]; then + echo "[landing build-docs] Installing docs deps (first run)..." + # Don't fail if the lockfile is stale in CI — fall back to a plain install. + pnpm --dir "$DOCS_DIR" --ignore-workspace install --frozen-lockfile \ + || pnpm --dir "$DOCS_DIR" --ignore-workspace install + fi + echo "[landing build-docs] Building VitePress site..." + pnpm --dir "$DOCS_DIR" --ignore-workspace build +fi + +if [ ! -d "$DIST_DIR" ]; then + echo "[landing build-docs] Expected dist at $DIST_DIR but it's missing. Did the build fail?" >&2 + exit 1 +fi + +echo "[landing build-docs] Staging dist → landing/public/docs/" +rm -rf "$PUBLIC_DOCS" +mkdir -p "$PUBLIC_DOCS" +# Copy contents (not the dir itself) so files land directly under +# public/docs/. -a preserves timestamps. +cp -a "$DIST_DIR"/. "$PUBLIC_DOCS"/ + +echo "[landing build-docs] Done. forge-pm.dev will serve docs at /docs/."