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
37 changes: 26 additions & 11 deletions site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ import { readFileSync } from "node:fs";
import { rehypeBaseLinks } from "./scripts/rehype-base-links.ts";
import { rehypeTableShell } from "./scripts/rehype-table-shell.ts";

// Served from https://arbiterforge.github.io/codeArbiter/ — shared by the
// `base` option below and the rehype plugin that base-prefixes markdown links.
const BASE = "/codeArbiter";
// Served from https://codearbiter.dev/ — shared by the `base` option below and
// the rehype plugin that base-prefixes markdown links.
//
// The value lives in ./base.mjs, the single source of truth, because the link
// auditor and several tests need it too and must never keep their own copy.
// See that file for why it is "" and not "/" on an apex domain. Re-exported
// here so importers of astro.config.mjs keep working unchanged.
export { BASE } from "./base.mjs";
import { BASE } from "./base.mjs";

// Build the reference sidebar groups from the generator's output. `predev` and
// `prebuild` run `npm run gen` first, so sidebar.json exists before this loads.
Expand All @@ -34,8 +40,8 @@ try {
}

export default defineConfig({
// GitHub Pages project site: served from https://arbiterforge.github.io/codeArbiter/.
// `base` also applies in local dev — the dev server serves at /codeArbiter/.
// GitHub Pages site on the apex custom domain: served from https://codearbiter.dev/.
// `base` also applies in local dev — the dev server serves at the root.
//
// BASE-PATH-SAFE LINK PATTERN for downstream authors:
// - Starlight does NOT rewrite root-absolute markdown links through the
Expand All @@ -44,18 +50,27 @@ export default defineConfig({
// - Instead, the `markdown.processor` below runs our local
// `rehypeBaseLinks(BASE)` plugin (scripts/rehype-base-links.ts) over
// every rendered page. It walks the HAST tree and prefixes any
// root-absolute href/src ("/overview") with BASE ("/codeArbiter/overview"),
// idempotently, so plain root-relative markdown links stay base-safe.
// root-absolute href/src ("/overview") with BASE, idempotently, so plain
// root-relative markdown links stay base-safe. On the apex domain BASE is
// "" and the prefixing is a no-op, but the plugin stays wired so the site
// survives a future move back to a subpath.
// - The plugin is passed to `unified({ ... })` from `@astrojs/markdown-remark`,
// NOT to the top-level `markdown.rehypePlugins` key. Astro 7.1 deprecated
// `markdown.remarkPlugins` / `rehypePlugins` / `remarkRehype` ("will be
// removed in a future major") — see the note on `markdown:` below.
// - In Astro component href props (not markdown), still use
// import.meta.env.BASE_URL: href={`${import.meta.env.BASE_URL}overview/`}
// - Never hardcode "/codeArbiter/" in href strings. That value desyncs
// when the Astro base is changed and is invisible to linting.
site: "https://arbiterforge.github.io",
base: BASE,
// - Diagram <img> tags in .md/.mdx are the documented exception: raw HTML in
// markdown is not walked by rehypeBaseLinks, so those srcs carry the base
// literally. test/generator/diagram-href-convention.test.ts is the guard,
// and it derives the expected prefix from BASE below rather than repeating
// it — when the base moved to "" for the apex domain, 19 such attributes
// across 16 pages had to change with it, and a hardcoded guard would have
// had to be edited by hand in lockstep.
site: "https://codearbiter.dev",
// `BASE || "/"`: Astro needs a real path, while rehypeBaseLinks needs the
// empty string (see the constant above for why "/" would corrupt links).
base: BASE || "/",
// Old `-2` skill URLs from before per-collection slug dedup (see generate.ts):
// these six skills shared a name with a same-named command, so the combined
// dedup pass pushed the skill page to a `-2` slug. Redirect the old URLs to
Expand Down
24 changes: 24 additions & 0 deletions site/base.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/** base.mjs — the site's base path, single source of truth.
*
* Imported by astro.config.mjs (which sets Astro's `base` and feeds
* rehypeBaseLinks), by scripts/link-audit/lib.ts, and by the tests that assert
* base-dependent output. It lives in its own module so none of those has to
* import the full Astro config just to learn one string.
*
* This file exists because the literal was previously copied into four places
* — astro.config.mjs, link-audit/lib.ts, and three test files — each with a
* comment saying it had to match the others. Moving the site to the
* codearbiter.dev apex domain desynced all four at once: four test failures and
* 19,940 link-audit failures from a one-line change.
*
* MUST be "" for an apex domain, never "/". rehypeBaseLinks prefixes every
* root-absolute href/src with this value, so "/" would rewrite
* "/diagrams/x.svg" into "//diagrams/x.svg" — a protocol-relative URL that
* resolves against a different host and fails silently rather than 404-ing.
* Empty string makes the prefixing a correct no-op. Consumers that need a real
* path (Astro's own `base`) use `BASE || "/"`.
*
* For a project subpath the value is "/<repo>" with no trailing slash, e.g.
* "/codeArbiter" when served from arbiterforge.github.io/codeArbiter/.
*/
export const BASE = "";
1 change: 1 addition & 0 deletions site/public/CNAME
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
codearbiter.dev
8 changes: 7 additions & 1 deletion site/scripts/link-audit/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@
import { readdirSync, statSync, readFileSync, existsSync } from "node:fs";
import { join, posix, relative, sep } from "node:path";

export const BASE = "/codeArbiter"; // must match astro.config.mjs `base`
// Single source of truth, shared with astro.config.mjs — never a local copy.
// This constant previously held its own literal with a "must match
// astro.config.mjs" comment, and the apex-domain move desynced it: 19,940
// link-audit failures from a one-line base change.
// @ts-expect-error -- untyped .mjs module
import { BASE as SHARED_BASE } from "../../base.mjs";
export const BASE: string = SHARED_BASE;

/** Recursively collect every *.html file under dir. */
export function htmlFiles(dir: string): string[] {
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/codearbiter-directory.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ frontmatter are both in place, the orchestrator persona loads on every session i

<figure class="ca-diagram">
<img
src="/codeArbiter/diagrams/activation-states.svg"
src="/diagrams/activation-states.svg"
alt="Activation classification flow: a missing CONTEXT.md or missing leading frontmatter is dormant; an unclosed block is malformed and surfaces an error; a closed block containing arbiter enabled activates the persona and gates."
loading="lazy"
width="900"
Expand Down
4 changes: 2 additions & 2 deletions site/src/content/docs/concepts/gated-lanes.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ by design; a hard gate that trips often is a signal the spec was too thin, and w
at the spec rather than working around at the gate.

<figure class="ca-diagram">
<img src="/codeArbiter/diagrams/gate-model.svg" alt="Gate model. A soft gate surfaces a decision bubble and waits for the user. A hard gate is a closed cross-bar that is never auto-decided." loading="lazy" />
<img src="/diagrams/gate-model.svg" alt="Gate model. A soft gate surfaces a decision bubble and waits for the user. A hard gate is a closed cross-bar that is never auto-decided." loading="lazy" />
<figcaption>Soft gates surface and wait. Hard gates stop, and only the user can clear them.</figcaption>
</figure>

Expand Down Expand Up @@ -53,7 +53,7 @@ commit. It's the only path to a commit. [`refactor`](/reference/skills/refactor/
phases, all built around proving behavioral parity through unmodified pre-existing tests.

<figure class="ca-diagram">
<img src="/codeArbiter/diagrams/commit-gate-phases.svg" alt="Commit-gate's nine phases as a left-to-right gated pipeline: permission, branch, classification, verification, behavioral proof, diff review, selective stage, message, commit. Each phase is a hard BLOCK gate. Phase 5.5, provenance auto-heal, is a conditional side-lane that only runs on drift, then rejoins before diff review." loading="lazy" />
<img src="/diagrams/commit-gate-phases.svg" alt="Commit-gate's nine phases as a left-to-right gated pipeline: permission, branch, classification, verification, behavioral proof, diff review, selective stage, message, commit. Each phase is a hard BLOCK gate. Phase 5.5, provenance auto-heal, is a conditional side-lane that only runs on drift, then rejoins before diff review." loading="lazy" />
<figcaption>Nine phases, permission through commit. 5.5 is a conditional side-lane, not a tenth gate.</figcaption>
</figure>

Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/concepts/jit-context-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ the read hook.
The hook is implemented as `pre-read.py`, backed by `_readinjectlib.py`; see the [hooks and gates reference](/reference/hooks-gates/) for the full gate catalog.

<figure class="ca-diagram">
<img src="/codeArbiter/diagrams/four-tier-map.svg" alt="The four-tier file-to-knowledge map: a Read is matched against security-controls.md, accepted ADRs, approved specs, and provenance in priority order, and the highest-priority match's pointer is injected within a 150-token budget." loading="lazy" />
<img src="/diagrams/four-tier-map.svg" alt="The four-tier file-to-knowledge map: a Read is matched against security-controls.md, accepted ADRs, approved specs, and provenance in priority order, and the highest-priority match's pointer is injected within a 150-token budget." loading="lazy" />
<figcaption>Four tiers, evaluated in priority order. The highest match governs; a non-governed Read injects nothing.</figcaption>
</figure>

Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/concepts/provenance-drift.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Re-baselining is an evidence claim, not a way to silence the warning. If you can
text remains correct, re-scout or defer.

<figure class="ca-diagram">
<img src="/codeArbiter/diagrams/provenance-drift-flow.svg" alt="Context-drift provenance flow: a tracked source change is detected by a git hash-object mismatch, surfaced at SessionStart, and healed by commit-gate which re-baselines or proposes a doc update with the work commit." loading="lazy" />
<img src="/diagrams/provenance-drift-flow.svg" alt="Context-drift provenance flow: a tracked source change is detected by a git hash-object mismatch, surfaced at SessionStart, and healed by commit-gate which re-baselines or proposes a doc update with the work commit." loading="lazy" />
<figcaption>Provenance tracks source hashes. A mismatch is detected at SessionStart and healed when the commit lands.</figcaption>
</figure>

Expand Down
4 changes: 2 additions & 2 deletions site/src/content/docs/enforcement.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ codeArbiter is dormant until a repository opts in. Every enforcement hook calls

<figure class="ca-diagram">
<img
src="/codeArbiter/diagrams/activation-states.svg"
src="/diagrams/activation-states.svg"
alt="Activation classification flow: a missing CONTEXT.md or missing leading frontmatter is dormant; an unclosed block is malformed and surfaces an error; a closed block containing arbiter enabled activates the persona and gates."
loading="lazy"
width="900"
Expand Down Expand Up @@ -106,7 +106,7 @@ codeArbiter runs untrusted repositories inside `ca-sandbox`, isolated as non-roo

<figure class="ca-diagram">
<img
src="/codeArbiter/diagrams/sandbox-boundary.svg"
src="/diagrams/sandbox-boundary.svg"
alt="ca-sandbox boundary: the untrusted repository lives in a Docker named volume inside a non-root, read-only container with dropped capabilities, no new privileges, resource limits, and network disabled by default. Host bind mounts and the Docker socket are blocked. Files leave only through host-initiated docker cp."
loading="lazy"
width="960"
Expand Down
4 changes: 2 additions & 2 deletions site/src/content/docs/feature-forge/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ promoted to **stable** and becomes on by default. The version says the whole pay
the forge says which individual features have earned trust.

<figure class="ca-art-banner ca-art-banner--forge">
<img src="/codeArbiter/art/feature-forge.webp" alt="" loading="lazy" />
<img src="/art/feature-forge.webp" alt="" loading="lazy" />
<figcaption>Preview is not unfinished. It is shipped work still earning the right to become the default.</figcaption>
</figure>

That is the two-axis model. Read them together and a release is legible: SemVer governs the
whole payload, the Feature Forge governs each feature's maturity.

<figure class="ca-diagram">
<img src="/codeArbiter/diagrams/two-axis-model.svg" alt="Two-axis labeling model. SemVer governs the whole plugin payload; the Feature Forge governs each feature as preview (opt-in, dormant) or stable (on by default, evidence-promoted)." loading="lazy" />
<img src="/diagrams/two-axis-model.svg" alt="Two-axis labeling model. SemVer governs the whole plugin payload; the Feature Forge governs each feature as preview (opt-in, dormant) or stable (on by default, evidence-promoted)." loading="lazy" />
<figcaption>Two axes: SemVer for the whole payload, the Feature Forge for per-feature maturity.</figcaption>
</figure>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ payloads; the live run proves Codex actually discovers, trusts, invokes, and hon
hooks.

<figure class="ca-diagram">
<img src="/codeArbiter/diagrams/core-fanout.svg" alt="Core fan-out: one shared source, core/pysrc plus core/surface, is deterministically generated by sync-core.py and build-surface.py into three host plugins — ca for Claude Code, ca-codex for Codex, and ca-pi for Pi. A CI byte-identity check gates every fan-out edge." loading="lazy" />
<img src="/diagrams/core-fanout.svg" alt="Core fan-out: one shared source, core/pysrc plus core/surface, is deterministically generated by sync-core.py and build-surface.py into three host plugins — ca for Claude Code, ca-codex for Codex, and ca-pi for Pi. A CI byte-identity check gates every fan-out edge." loading="lazy" />
<figcaption>One source, generated into three host plugins; a CI byte-identity check gates every fan-out edge.</figcaption>
</figure>

Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/guides/adding-a-dependency.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Route every new dependency through `/ca:add-dep` before touching the package man

<figure class="ca-diagram">
<img
src="/codeArbiter/diagrams/lane-add-dep.svg"
src="/diagrams/lane-add-dep.svg"
alt="The /ca:add-dep lane in two rows: Commands (/ca:add-dep) and Agents (dependency-reviewer), with a connector from the command to the agent. The skills row is omitted because this lane uses no skills."
loading="lazy"
width="920"
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/guides/autonomous-sprints.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ authorized to merge.

<figure class="ca-diagram">
<img
src="/codeArbiter/diagrams/lane-sprint.svg"
src="/diagrams/lane-sprint.svg"
alt="The autonomous sprint lane from one combined spec and plan approval through small implementation cells, SMARTS-logged decisions, review, verification, commit, and pull request."
loading="lazy"
/>
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/guides/feature-lane.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ and the author and reviewer agents it dispatches on demand.

<figure class="ca-diagram">
<img
src="/codeArbiter/diagrams/lane-feature.svg"
src="/diagrams/lane-feature.svg"
alt="The /ca:feature lane in three rows: Commands (/ca:feature, /ca:commit, /ca:pr), Skills (brainstorming, writing-plans, tdd, commit-gate), and Agents (author agents, reviewer agents), with a connector weaving through them in execution order."
loading="lazy"
width="920"
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/guides/opt-in-a-repo.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The plugin installs once, globally. Enabling enforcement is a per-repo step you

<figure class="ca-diagram">
<img
src="/codeArbiter/diagrams/lane-opt-in.svg"
src="/diagrams/lane-opt-in.svg"
alt="The /ca:init lane in two rows: Commands (/ca:init, which forks to /ca:create-context for existing code or /ca:decompose for a new project) and Skills (create-context backs the first path, decompose the second). Two connectors fork from /ca:init, one to each path."
loading="lazy"
width="920"
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/guides/recording-adrs.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ linked to the paths it governs.

<figure class="ca-diagram">
<img
src="/codeArbiter/diagrams/lane-adr.svg"
src="/diagrams/lane-adr.svg"
alt="The architecture decision lane from a decision prompt through numbered ADR authoring, user attribution, validation, and the decision log."
loading="lazy"
/>
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/guides/releasing-a-version.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ already landed on a clean release branch and its verification is green.

<figure class="ca-diagram">
<img
src="/codeArbiter/diagrams/lane-release.svg"
src="/diagrams/lane-release.svg"
alt="The release lane from selecting one plugin target through derived semantic version, changelog and manifest updates, verification, annotated tag, and explicitly authorized publication."
loading="lazy"
/>
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/guides/the-statusline.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ dependency-free (native font glyphs only, no Nerd Font) and renders on every ses
<figure class="ca-diagram">
<div class="ca-statusline-map">
<img
src="/codeArbiter/diagrams/statusline.png"
src="/diagrams/statusline.png"
alt="The codeArbiter statusline: a folder row; a git row with repo, branch, and a model pill; rate-limit percentages with reset countdowns; an arbiter row showing stage, tasks, open questions, and overrides; and session/today token, cost, burn, and context-usage segments."
loading="lazy"
width="2174"
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/guides/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ If the file is absent, run `/ca:init` to scaffold it.

<figure class="ca-diagram">
<img
src="/codeArbiter/diagrams/activation-states.svg"
src="/diagrams/activation-states.svg"
alt="Activation classification flow: a missing CONTEXT.md or missing leading frontmatter is dormant; an unclosed block is malformed and surfaces an error; a closed block containing arbiter enabled activates the persona and gates."
loading="lazy"
width="900"
Expand Down
2 changes: 1 addition & 1 deletion site/src/content/docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ your behalf.
branch only through a pull request. Never a direct write.

<figure class="ca-diagram">
<img src="/codeArbiter/diagrams/lane-flow.svg" alt="Lane flow: a command invocation routes to the owning skill, clears its gate, then ships to version control." loading="lazy" />
<img src="/diagrams/lane-flow.svg" alt="Lane flow: a command invocation routes to the owning skill, clears its gate, then ships to version control." loading="lazy" />
<figcaption>One lane, five steps: command, route, gate, ship to a PR.</figcaption>
</figure>

Expand Down
12 changes: 10 additions & 2 deletions site/test/astro-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ import { isUnifiedProcessor } from "@astrojs/markdown-remark";
// @ts-expect-error -- untyped .mjs config module
import config from "../astro.config.mjs";

const BASE = "/codeArbiter";
// @ts-expect-error -- untyped .mjs config module
import { BASE } from "../astro.config.mjs";
Comment thread
SUaDtL marked this conversation as resolved.

describe("astro.config markdown wiring", () => {
it("configures no markdown plugins through the deprecated top-level keys", () => {
Expand All @@ -41,7 +42,14 @@ describe("astro.config markdown wiring", () => {
const { code } = await renderer.render("[Overview](/overview)");

expect(code).toContain(`href="${BASE}/overview"`);
expect(code).not.toContain(`href="/overview"`);

// Only meaningful when the site is served from a subpath. On the apex

// domain BASE is "" and the prefixing is a correct no-op, so asserting the

// bare form is absent would assert the opposite of the intended behaviour.

if (BASE !== "") expect(code).not.toContain(`href="/overview"`);
});

it("leaves an external link untouched through the configured processor", async () => {
Expand Down
6 changes: 4 additions & 2 deletions site/test/content/documentation-presentation.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
// @ts-expect-error -- untyped .mjs config module
import { BASE } from "../../astro.config.mjs";

const siteRoot = process.cwd();
const docsRoot = join(siteRoot, "src", "content", "docs");
Expand Down Expand Up @@ -96,8 +98,8 @@ describe("documentation presentation regressions", () => {
const artwork = join(siteRoot, "public", "art", "feature-forge.webp");

expect(forge).toContain('class="ca-art-banner');
expect(forge).toContain('/codeArbiter/art/feature-forge.webp');
expect(forge).toContain('/codeArbiter/diagrams/two-axis-model.svg');
expect(forge).toContain(`${BASE}/art/feature-forge.webp`);
expect(forge).toContain(`${BASE}/diagrams/two-axis-model.svg`);
expect(existsSync(artwork)).toBe(true);
expect(themeStyles).toContain(".ca-art-banner");
});
Expand Down
Loading
Loading