docs: own public documentation metadata (#1713)

This commit is contained in:
José Almeida
2026-07-16 08:32:40 +01:00
committed by GitHub
parent 2a1f7c2edf
commit 66e03d5287
22 changed files with 509 additions and 3 deletions
+7 -2
View File
@@ -14,7 +14,8 @@ Use this skill to decide whether public docs need updates and to make those upda
- Implementation plans stay under `docs/plans/**`. - Implementation plans stay under `docs/plans/**`.
- Architecture decisions stay under `docs/decisions/**`. - Architecture decisions stay under `docs/decisions/**`.
- Raw supporting notes can remain under `docs/**` outside `docs/public/**`, but do not publish them unless rewritten for users. - Raw supporting notes can remain under `docs/**` outside `docs/public/**`, but do not publish them unless rewritten for users.
- The landing/docs website consumes public docs through its manifest and generated content. Do not hand-edit generated website docs. - `docs/public/meta.json` owns published-page order and navigation groups. Page paths own routes, and page frontmatter owns titles and descriptions.
- The landing/docs website generates its content from this directory. Do not hand-edit generated files in the landing repository.
## When Docs Need Updates ## When Docs Need Updates
@@ -39,6 +40,7 @@ Skip public docs when the change is:
2. Search `docs/public/**` first for affected terms and commands. 2. Search `docs/public/**` first for affected terms and commands.
3. If public docs exist, update them with the same PR as the behavior change. 3. If public docs exist, update them with the same PR as the behavior change.
4. If no public docs exist but the behavior is user-facing, add or propose the smallest useful public page/section. 4. If no public docs exist but the behavior is user-facing, add or propose the smallest useful public page/section.
When adding a page, include `title` and `description` frontmatter and list its page slug or path without the `.md` extension in `docs/public/meta.json` exactly once, for example `cli`. See `docs/public/README.md`.
5. If the change only updates implementation intent or architectural history, update specs/plans/ADRs instead. 5. If the change only updates implementation intent or architectural history, update specs/plans/ADRs instead.
6. Keep public docs task-oriented: prerequisites, commands, expected result, troubleshooting, and links to reference. 6. Keep public docs task-oriented: prerequisites, commands, expected result, troubleshooting, and links to reference.
7. Preserve internal links inside `docs/public/**` where possible. Link to source-only raw docs only when the raw note is intentionally not published. 7. Preserve internal links inside `docs/public/**` where possible. Link to source-only raw docs only when the raw note is intentionally not published.
@@ -51,13 +53,16 @@ Run the checks relevant to your change:
```bash ```bash
# Replace SEARCH_TERM with the command, config key, or terminology that changed. # Replace SEARCH_TERM with the command, config key, or terminology that changed.
rg -n "SEARCH_TERM" docs/public docs/specs docs/decisions rg -n "SEARCH_TERM" docs/public docs/specs docs/decisions
node --test scripts/validate-public-docs.test.mjs
node scripts/validate-public-docs.mjs
``` ```
For website docs publishing changes, also run from the landing repo: For website docs publishing changes, also run from the landing repo:
```bash ```bash
pnpm install --frozen-lockfile
pnpm --filter @kandev/docs fetch-docs pnpm --filter @kandev/docs fetch-docs
pnpm exec vitest run apps/docs/lib/docs-processing.test.ts pnpm exec vitest run apps/docs/lib/docs-processing.test.ts apps/docs/lib/public-docs.test.ts
pnpm --filter @kandev/docs build pnpm --filter @kandev/docs build
``` ```
+65
View File
@@ -0,0 +1,65 @@
name: Published Docs
on:
push:
branches: [main]
paths:
- "docs/public/**"
pull_request:
branches: [main]
paths:
- "docs/public/**"
- "scripts/validate-public-docs.mjs"
- "scripts/validate-public-docs.test.mjs"
- ".github/workflows/notify-docs.yml"
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
validate:
name: Validate public docs
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
with:
persist-credentials: false
- name: Test public docs validator
run: node --test scripts/validate-public-docs.test.mjs
- name: Validate public docs
run: node scripts/validate-public-docs.mjs
rebuild:
name: Trigger Cloudflare Pages rebuild
needs: validate
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 4
steps:
- name: Trigger Cloudflare Pages deploy hook
env:
DEPLOY_HOOK_URL: ${{ secrets.CLOUDFLARE_PAGES_DEPLOY_HOOK_KANDEV_AI }}
run: |
if [[ -z "$DEPLOY_HOOK_URL" ]]; then
echo "::error::CLOUDFLARE_PAGES_DEPLOY_HOOK_KANDEV_AI is not configured"
exit 1
fi
curl --fail --silent --show-error \
--max-time 30 \
--retry 3 \
--retry-all-errors \
--retry-max-time 90 \
--request POST \
"$DEPLOY_HOOK_URL"
+1 -1
View File
@@ -14,7 +14,7 @@ You must understand the code you submit. You're welcome to use AI tools to help
1. **Fork and branch.** Create a feature branch from `main`. 1. **Fork and branch.** Create a feature branch from `main`.
2. **Keep PRs focused.** One logical change per PR. Small PRs get reviewed faster. 2. **Keep PRs focused.** One logical change per PR. Small PRs get reviewed faster.
3. **Update public docs when behavior changes.** User-facing docs live in `docs/public/**`. If your change affects CLI commands, config keys, install/deploy flows, workflows, executors, APIs, screenshots, or user-facing terminology, update the relevant public docs in the same PR. 3. **Update public docs when behavior changes.** User-facing docs live in `docs/public/**`. If your change affects CLI commands, config keys, install/deploy flows, workflows, executors, APIs, screenshots, or user-facing terminology, update the relevant public docs in the same PR. See the [public docs contribution guide](docs/public/README.md) when editing navigation or adding a page.
4. **Test your changes.** Run `make fmt` first, then `make typecheck test lint` before submitting. Manually verify that your feature works end-to-end, and add screenshots or recordings to the PR if it has a UI component. **If your change touches any UI files (anything under `apps/web/`), you must add or update Playwright e2e tests in `apps/web/e2e/` to prevent regressions.** Run them with `make test-e2e`. See [docs/test_e2e_web.md](docs/test_e2e_web.md) for patterns and fixtures. 4. **Test your changes.** Run `make fmt` first, then `make typecheck test lint` before submitting. Manually verify that your feature works end-to-end, and add screenshots or recordings to the PR if it has a UI component. **If your change touches any UI files (anything under `apps/web/`), you must add or update Playwright e2e tests in `apps/web/e2e/` to prevent regressions.** Run them with `make test-e2e`. See [docs/test_e2e_web.md](docs/test_e2e_web.md) for patterns and fixtures.
## Bug Reports ## Bug Reports
+1
View File
@@ -427,6 +427,7 @@ test-scripts:
@python3 scripts/lint-harness-files.test.py @python3 scripts/lint-harness-files.test.py
@bash scripts/release-desktop.test.sh @bash scripts/release-desktop.test.sh
@node --test apps/desktop/e2e/desktop-launch-smoke.test.mjs @node --test apps/desktop/e2e/desktop-launch-smoke.test.mjs
@node --test scripts/validate-public-docs.test.mjs
.PHONY: test-e2e .PHONY: test-e2e
test-e2e: build-backend build-web test-e2e: build-backend build-web
+47
View File
@@ -0,0 +1,47 @@
# Contributing to the Public Docs
Files in this directory are the source for [kandev.ai/docs](https://kandev.ai/docs). A merged change under `docs/public/**` triggers a Cloudflare Pages rebuild automatically.
## Update an Existing Page
Edit the relevant Markdown file directly. Keep commands, configuration keys, API names, and screenshots aligned with the implementation in the same pull request.
Use relative links between published pages, for example `[CLI](cli.md)`. The website build rewrites those links to `/docs/...` routes. Links to repository files outside `docs/public` are rewritten to their GitHub source URLs.
## Add a Page
1. Create a lowercase, kebab-case Markdown file in this directory, such as `custom-executors.md`. Its filename becomes its route: `custom-executors.md` is published at `/docs/custom-executors`.
2. Start the file with `title` and `description` frontmatter, followed by one level-one heading:
```markdown
---
title: "Custom Executors"
description: "Configure a custom executor for Kandev tasks."
---
# Custom Executors
Page content starts here.
```
3. Add the filename without its extension to `meta.json` exactly once. Its position controls sidebar order. Entries such as `---Execution---` create section labels.
4. Link the new page from related documentation where useful.
Pages omitted from `meta.json`, duplicate entries, unknown entries, and missing frontmatter fail validation.
## Validate
Run the dependency-free checks from the Kandev repository root:
```bash
node --test scripts/validate-public-docs.test.mjs
node scripts/validate-public-docs.mjs
```
To build the complete docs site, clone `kdlbs/landing` beside this repository and run:
```bash
cd ../landing
pnpm install --frozen-lockfile
KANDEV_DOCS_SOURCE_PATH=../kandev/docs pnpm --filter @kandev/docs build
```
+5
View File
@@ -1,3 +1,8 @@
---
title: "Add an Agent CLI"
description: "Add a new ACP adapter or agent CLI integration to Kandev."
---
# Adding a New Agent CLI Integration # Adding a New Agent CLI Integration
This guide explains how to add support for a new coding agent CLI to Kandev. This guide explains how to add support for a new coding agent CLI to Kandev.
+5
View File
@@ -1,3 +1,8 @@
---
title: "CLI"
description: "Install, start, and operate Kandev from the command line."
---
# Kandev CLI # Kandev CLI
## Architecture ## Architecture
+5
View File
@@ -1,3 +1,8 @@
---
title: "Configuration"
description: "Configure Kandev workspaces, runtimes, settings, and environment behavior."
---
# Configuration # Configuration
Kandev's backend reads configuration from three sources, in this order of precedence (later sources override earlier ones): Kandev's backend reads configuration from three sources, in this order of precedence (later sources override earlier ones):
+5
View File
@@ -1,3 +1,8 @@
---
title: "Desktop App"
description: "Install and run the Tauri desktop app with the native Kandev runtime."
---
# Kandev Desktop App # Kandev Desktop App
Kandev desktop is a Tauri app that starts the native Kandev runtime locally and shows the existing Kandev UI in a system WebView. It does not require Node.js at runtime. Node.js, pnpm, Rust, and the Tauri CLI are build-time tools only. Kandev desktop is a Tauri app that starts the native Kandev runtime locally and shows the existing Kandev UI in a system WebView. It does not require Node.js at runtime. Node.js, pnpm, Rust, and the Tauri CLI are build-time tools only.
+5
View File
@@ -1,3 +1,8 @@
---
title: "Docker"
description: "Run Kandev and agent execution environments with Docker."
---
# Docker Guide # Docker Guide
Run Kandev in a Docker container. For Kubernetes deployment, see [k8s.md](k8s.md). Run Kandev in a Docker container. For Kubernetes deployment, see [k8s.md](k8s.md).
+5
View File
@@ -1,3 +1,8 @@
---
title: "Git Operations"
description: "Understand how Kandev manages repositories, worktrees, branches, and review surfaces."
---
# Git Operations for Worktrees # Git Operations for Worktrees
This document describes the architecture for Git operations (Pull, Push, Rebase, Merge, and Abort) on worktrees in Kandev. This document describes the architecture for Git operations (Pull, Push, Rebase, Merge, and Abort) on worktrees in Kandev.
+5
View File
@@ -1,3 +1,8 @@
---
title: "Overview"
description: "Start building with Kandev agentic development workflows."
---
# Kandev Feature Guide # Kandev Feature Guide
This page expands the short feature list in the README without turning the README into a catalog. This page expands the short feature list in the README without turning the README into a catalog.
+5
View File
@@ -1,3 +1,8 @@
---
title: "Kubernetes"
description: "Deploy Kandev in Kubernetes environments."
---
# Kubernetes Deployment Guide # Kubernetes Deployment Guide
This guide covers building the Kandev Docker image and deploying it to a Kubernetes cluster. This guide covers building the Kandev Docker image and deploying it to a Kubernetes cluster.
+23
View File
@@ -0,0 +1,23 @@
{
"title": "Kandev Docs",
"pages": [
"---Start---",
"index",
"cli",
"configuration",
"desktop-app",
"---Execution---",
"docker",
"k8s",
"run-as-a-service",
"remote-cloud-environment",
"windows-support",
"---Workflows---",
"workflow-tips",
"workflow-import-export",
"git-operations",
"add-agent-cli",
"---Reference---",
"websocket-api"
]
}
+5
View File
@@ -1,3 +1,8 @@
---
title: "Remote Cloud Environment"
description: "Use remote execution environments for agent work."
---
# Remote Cloud Environment Instructions # Remote Cloud Environment Instructions
Setup notes and caveats for developing Kandev in ephemeral cloud VMs (Cursor Cloud, Codex, GitHub Codespaces, or similar sandboxed environments). Setup notes and caveats for developing Kandev in ephemeral cloud VMs (Cursor Cloud, Codex, GitHub Codespaces, or similar sandboxed environments).
+5
View File
@@ -1,3 +1,8 @@
---
title: "Run as a Service"
description: "Run Kandev as a persistent service on your own machine or infrastructure."
---
# Run Kandev as a Service # Run Kandev as a Service
Install Kandev as an OS-managed service (systemd on Linux, launchd on macOS) so it auto-starts and stays running. User-mode services installed by `kandev service install` can self-update from the System → Updates page. Non-service installs and `--system` services still update manually: `npm i -g kandev@latest` or `brew upgrade kandev`, then re-run `kandev service install`. Install Kandev as an OS-managed service (systemd on Linux, launchd on macOS) so it auto-starts and stays running. User-mode services installed by `kandev service install` can self-update from the System → Updates page. Non-service installs and `--system` services still update manually: `npm i -g kandev@latest` or `brew upgrade kandev`, then re-run `kandev service install`.
+5
View File
@@ -1,3 +1,8 @@
---
title: "WebSocket API"
description: "Reference for Kandev WebSocket messages and API behavior."
---
# Kandev WebSocket API # Kandev WebSocket API
This page documents the WebSocket envelope, current domain model, and the request actions used by the main task and agent workflow. It is not a substitute for the Go request types: payloads evolve with the application. This page documents the WebSocket envelope, current domain model, and the request actions used by the main task and agent workflow. It is not a substitute for the Go request types: payloads evolve with the application.
+5
View File
@@ -1,3 +1,8 @@
---
title: "Windows Support"
description: "Run Kandev on Windows through native and WSL workflows."
---
# Windows Support (WSL) # Windows Support (WSL)
Kandev runs on Windows through WSL (Windows Subsystem for Linux). Kandev runs on Windows through WSL (Windows Subsystem for Linux).
+5
View File
@@ -1,3 +1,8 @@
---
title: "Workflow Import and Export"
description: "Move workflows between workspaces or Kandev installs with portable YAML."
---
# Workflow Import / Export — Portable YAML Format # Workflow Import / Export — Portable YAML Format
Kandev workflows can be exported to a portable YAML file and imported into Kandev workflows can be exported to a portable YAML file and imported into
+5
View File
@@ -1,3 +1,8 @@
---
title: "Workflow Tips"
description: "Practical guidance for designing and operating Kandev workflows."
---
# Workflows # Workflows
Kandev ships with five workflow templates. Each defines a sequence of steps with automated transitions - agents start, stop, and move between steps based on events like entering a step, sending a message, or an agent completing its turn. Kandev ships with five workflow templates. Each defines a sequence of steps with automated transitions - agents start, stop, and move between steps based on events like entering a step, sending a message, or an agent completing its turn.
+148
View File
@@ -0,0 +1,148 @@
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
const repoRoot = path.resolve(
path.dirname(fileURLToPath(import.meta.url)),
"..",
);
/**
* Validate that every published Markdown page has frontmatter and appears
* exactly once in the public navigation metadata.
*
* @param {string} [docsDir] Directory containing published docs and meta.json.
* @returns {Promise<{pageCount: number}>} Number of validated published pages.
*/
export async function validatePublicDocs(
docsDir = path.join(repoRoot, "docs/public"),
) {
const meta = await readMeta(docsDir);
const files = await collectMarkdownFiles(docsDir);
const pagesBySlug = new Map();
for (const file of files) {
if (path.posix.basename(file).toLowerCase() === "readme.md") continue;
const markdown = await fs.readFile(path.join(docsDir, file), "utf8");
assertFrontmatter(file, markdown);
const slug = file.replace(/\.mdx?$/, "").replace(/\/index$/, "");
const existing = pagesBySlug.get(slug);
if (existing) {
throw new Error(
`multiple published files resolve to slug ${slug}: ${existing}, ${file}`,
);
}
pagesBySlug.set(slug, file);
}
const listed = new Set();
for (const entry of meta.pages) {
if (isNavigationDecoration(entry)) continue;
if (listed.has(entry)) {
throw new Error(`meta.json lists page more than once: ${entry}`);
}
if (!pagesBySlug.has(entry)) {
throw new Error(`meta.json references unknown page: ${entry}`);
}
listed.add(entry);
}
for (const slug of pagesBySlug.keys()) {
if (!listed.has(slug)) {
throw new Error(`meta.json is missing published page: ${slug}`);
}
}
return { pageCount: pagesBySlug.size };
}
/**
* Read and validate the shape of public navigation metadata.
*
* @param {string} docsDir Directory containing meta.json.
* @returns {Promise<{pages: string[]}>} Parsed navigation metadata.
*/
async function readMeta(docsDir) {
const raw = await fs.readFile(path.join(docsDir, "meta.json"), "utf8");
const meta = JSON.parse(raw);
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
throw new Error("meta.json must contain a JSON object");
}
if (
!Array.isArray(meta.pages) ||
!meta.pages.every((entry) => typeof entry === "string")
) {
throw new Error("meta.json pages must be an array of strings");
}
return meta;
}
/**
* Recursively collect Markdown paths relative to the published docs root.
*
* @param {string} dir Published docs root.
* @param {string} [relativeDir] Directory relative to the docs root.
* @returns {Promise<string[]>} Sorted relative Markdown paths.
*/
async function collectMarkdownFiles(dir, relativeDir = "") {
const entries = await fs.readdir(path.join(dir, relativeDir), {
withFileTypes: true,
});
const files = await Promise.all(
entries.map(async (entry) => {
const relativePath = path.posix.join(relativeDir, entry.name);
if (entry.isDirectory()) {
return collectMarkdownFiles(dir, relativePath);
}
return /\.mdx?$/.test(entry.name) ? [relativePath] : [];
}),
);
return files.flat().sort();
}
/**
* Require non-empty title and description fields in leading frontmatter.
*
* @param {string} file Relative page path used in validation errors.
* @param {string} markdown Page source.
* @returns {void}
*/
function assertFrontmatter(file, markdown) {
const block = markdown.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
if (
!block ||
!/^title:\s*\S.*$/m.test(block) ||
!/^description:\s*\S.*$/m.test(block)
) {
throw new Error(
`${file} must start with YAML frontmatter containing title and description`,
);
}
}
/**
* Return whether a metadata entry is a navigation heading.
*
* @param {string} entry Navigation metadata entry.
* @returns {boolean} Whether the entry is a heading decoration.
*/
function isNavigationDecoration(entry) {
return /^---.*---$/.test(entry);
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
validatePublicDocs()
.then(({ pageCount }) =>
console.log(`Validated ${pageCount} published docs pages.`),
)
.catch((error) => {
console.error(error.message);
process.exitCode = 1;
});
}
+147
View File
@@ -0,0 +1,147 @@
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test, { after } from "node:test";
import { validatePublicDocs } from "./validate-public-docs.mjs";
const tempDirs = [];
/**
* Create an isolated published-docs fixture.
*
* @param {Record<string, string>} files Fixture files keyed by relative path.
* @param {{pages: string[]}} meta Navigation metadata.
* @returns {Promise<string>} Temporary fixture directory.
*/
async function createDocs(files, meta) {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "kandev-public-docs-"));
tempDirs.push(dir);
await Promise.all(
Object.entries(files).map(async ([file, content]) => {
const target = path.join(dir, file);
await fs.mkdir(path.dirname(target), { recursive: true });
await fs.writeFile(target, content);
}),
);
await fs.writeFile(path.join(dir, "meta.json"), JSON.stringify(meta));
return dir;
}
after(async () => {
await Promise.all(
tempDirs.map((dir) => fs.rm(dir, { recursive: true, force: true })),
);
});
const validPage = `---
title: "Overview"
description: "Start using Kandev."
---
# Kandev
Page body.
`;
test("accepts explicitly ordered pages with required frontmatter", async () => {
const dir = await createDocs(
{
"README.md": "# Contributing",
"index.md": validPage,
"cli.md": validPage,
},
{ title: "Kandev Docs", pages: ["---Start---", "index", "cli"] },
);
await assert.doesNotReject(validatePublicDocs(dir));
});
test("rejects published pages omitted from meta.json", async () => {
const dir = await createDocs(
{ "index.md": validPage, "cli.md": validPage },
{ pages: ["index"] },
);
await assert.rejects(
validatePublicDocs(dir),
/meta.json is missing published page: cli/,
);
});
test("rejects meta.json entries without a matching file", async () => {
const dir = await createDocs(
{ "index.md": validPage },
{ pages: ["index", "nonexistent"] },
);
await assert.rejects(
validatePublicDocs(dir),
/meta.json references unknown page: nonexistent/,
);
});
test("rejects unsupported link decorations as unknown pages", async () => {
const dir = await createDocs(
{ "index.md": validPage },
{ pages: ["index", "external:[Support](https://example.com)"] },
);
await assert.rejects(
validatePublicDocs(dir),
/meta.json references unknown page: external:\[Support\]\(https:\/\/example.com\)/,
);
});
test("rejects duplicate entries in meta.json", async () => {
const dir = await createDocs(
{ "index.md": validPage },
{ pages: ["index", "index"] },
);
await assert.rejects(
validatePublicDocs(dir),
/meta.json lists page more than once: index/,
);
});
test("rejects files that resolve to the same published slug", async () => {
const dir = await createDocs(
{ "foo.md": validPage, "foo/index.md": validPage },
{ pages: ["foo"] },
);
await assert.rejects(
validatePublicDocs(dir),
/multiple published files resolve to slug foo: foo.md, foo\/index.md/,
);
});
test("accepts single-character frontmatter values", async () => {
const dir = await createDocs(
{
"index.md": `---
title: x
description: y
---
# X
`,
},
{ pages: ["index"] },
);
await assert.doesNotReject(validatePublicDocs(dir));
});
test("rejects published pages without title and description frontmatter", async () => {
const dir = await createDocs(
{ "index.md": "# Kandev\n" },
{ pages: ["index"] },
);
await assert.rejects(
validatePublicDocs(dir),
/index.md must start with YAML frontmatter containing title and description/,
);
});