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
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
mkdocs==1.6.1
mkdocs-material==9.6.21
mkdocs-llmstxt==0.3.0
mkdocs-git-revision-date-localized-plugin==1.5.3
4 changes: 2 additions & 2 deletions docs/site/concepts/gates-and-decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A gate is the decision point at the end of a stage where nothing advances withou

Each call you make sharpens the bar, and the destination is delegation. When you are sufficiently confident in the workflow and the bar you set, hand over the conn and let the first officer drive multiple tasks with auto-approval:

```
```text
you have the conn to drive toward your sprint goal, authorized to approve and
merge PR on CI green. use your judgement.
```
Expand All @@ -15,7 +15,7 @@ Until then, the gates are yours.

A gate review has a fixed spine: the first three lines and the last line carry the decision; everything between is supporting evidence. If you stop reading after line three, you can still vote.

```
```text
Gate review: Fix the flaky login test — review
Chosen direction: replace sleep-based waits with event polling
Recommend reject: the AC-2 retry scenario has no covering test.
Expand Down
2 changes: 1 addition & 1 deletion docs/site/get-started/first-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ On accept, commission dispatches your seed items in parallel, each moving
through the stages until everything is idle or waiting on you. When a work
item reaches the `review` gate, you get a gate review:

```
```text
Gate review: Add rate limiting to the API — review
Chosen direction: token-bucket limiter at the API middleware layer
Recommend approve.
Expand Down
2 changes: 1 addition & 1 deletion docs/site/reference/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The `spacedock` binary groups its subcommands into Launch, Setup, and Workflow,

`spacedock --version` prints the version and contract level, the sandbox posture, then a per-runtime line reporting the installed spacedock plugin version:

```
```text
spacedock 0.20.1 (contract 1)
Sandbox: available, not enabled (no .safehouse profile)
claude: spacedock 0.20.1
Expand Down
39 changes: 39 additions & 0 deletions docs/site/reference/glossary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Glossary

The core Spacedock terms, in one place. Each links to where the concept is covered in depth.

## Workflow

A directory plus a `README.md` that declares the stages a work item moves through and what each stage expects. A project can hold more than one. See [Workflows and entities](../concepts/workflows-and-entities.md).

## Entity

One work item, stored as a single markdown file inside a workflow. Its frontmatter holds the machine-readable state (id, current stage, outcome); its body holds the problem, design notes, the bar for done, and the reports filed as it advances. See [Workflows and entities](../concepts/workflows-and-entities.md).

## Stage

A named step in a workflow (for example `design`, `implementation`, `review`). The README defines what each stage means, what counts as good, and what a worker must produce. See [The stage lifecycle](../concepts/stage-lifecycle.md).

## Gate

The decision point at the end of a stage where nothing advances without your vote. When a stage declares a gate, the first officer stops, presents a review, and waits for you to approve, redo with feedback, or reject. See [Gates and decisions](../concepts/gates-and-decisions.md).

## First officer

The orchestrator agent. It runs the workflow for you: finds what is ready, dispatches workers, checks results against the bar you set, and brings each gate decision to you with evidence. There is one per session. See [The operating model](../concepts/operating-model.md).

## Ensign

The worker agent. It moves one work item through one stage and proves the work, then signals completion. Ensigns come and go with the work. See [The operating model](../concepts/operating-model.md).

## Mod

An opt-in extension that adds behavior to a workflow without changing its stages, such as the [pr-merge mod](../advanced/mods-and-standing-teammates.md) that manages the pull-request lifecycle so merging never has to be a stage. See [Mods and standing teammates](../advanced/mods-and-standing-teammates.md).

## Standing teammate

A long-lived agent the first officer keeps around across dispatches for a recurring role (for example a reviewer), rather than spawning a fresh worker each time. See [Mods and standing teammates](../advanced/mods-and-standing-teammates.md).

## Split-root state

A layout that keeps a workflow's mutable state (entity bodies, stage reports) in a separate state checkout from the code, so concurrent workers commit state without colliding on the code branch. See [Split-root state](../advanced/split-root-state.md).
2 changes: 1 addition & 1 deletion docs/site/running-workflows/commission.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ A workflow is designed in conversation: you make four decisions and commission d

Invoke it from a session started with `spacedock claude`. You can pass the mission inline:

```
```text
/spacedock:commission product idea to simulated customer interview
```

Expand Down
137 changes: 137 additions & 0 deletions hooks/enrich_md_mirrors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# ABOUTME: MkDocs hook that enriches the per-page .md mirrors emitted by
# ABOUTME: mkdocs-llmstxt with YAML frontmatter and a ## Sitemap section.
"""Enrich the llmstxt per-page markdown mirrors for agent-readability.

mkdocs-llmstxt writes a markdown mirror of every page at
``<site_dir>/<page.url>index.md`` in its own ``on_post_build``. Those mirrors
carry no frontmatter and no sitemap section, and the plugin offers no config to
inject them. This hook collects each page's metadata during the build, then in
its own ``on_post_build`` (which MkDocs runs after the plugin's, because hooks
register after the ``plugins`` list) prepends a YAML frontmatter block
(``title``, ``description``, ``doc_version``, ``last_updated``) and appends a
short ``## Sitemap`` section of sibling-page links.

Clears the agent-readability ``markdown.frontmatter`` and
``markdown.sitemap-section`` checks.
"""

import json
import os
from pathlib import Path

# Per-page metadata gathered during the build, keyed by the page's output URL
# (e.g. "concepts/workflows-and-entities/", or "" for the home page). Populated
# in on_page_content; consumed in on_post_build.
_PAGES = {}

# Site-level config captured in on_config for use in on_post_build.
_CONFIG = {}


def on_config(config, **kwargs):
_CONFIG["site_description"] = config.get("site_description", "")
_CONFIG["doc_version"] = (config.get("extra", {}) or {}).get("doc_version", "")
_CONFIG["site_dir"] = config["site_dir"]
return config


def on_page_content(html, page, config, files, **kwargs):
"""Capture each page's frontmatter inputs and its sibling links.

Runs after the page is rendered, so page.meta (including the git-revision
date) and page.parent are fully populated.
"""
meta = page.meta or {}

title = (page.title or "").strip()
description = (meta.get("description") or _CONFIG.get("site_description") or "").strip()

last_updated = (
meta.get("git_revision_date_localized_raw_iso_datetime")
or meta.get("git_revision_date_localized_raw_datetime")
or ""
)

# Sibling pages within the same nav section, linked to their .md mirrors.
siblings = []
parent = page.parent
if parent is not None and getattr(parent, "children", None):
for child in parent.children:
if getattr(child, "is_page", False) and child.url and child.url != page.url:
child_title = (child.title or "").strip()
# Mirror lives at <child.url>index.md, relative to this mirror.
href = _relative_mirror_href(page.url, child.url)
siblings.append((child_title, href))

_PAGES[page.url] = {
"title": title,
"description": description,
"last_updated": last_updated,
"siblings": siblings,
}
return html


def on_post_build(config, **kwargs):
"""Rewrite each emitted .md mirror with frontmatter + a Sitemap section."""
site_dir = Path(_CONFIG["site_dir"])
doc_version = _CONFIG.get("doc_version", "")

for url, data in _PAGES.items():
mirror = site_dir / url / "index.md"
if not mirror.is_file():
continue

body = mirror.read_text(encoding="utf-8")

frontmatter = _build_frontmatter(
title=data["title"],
description=data["description"],
doc_version=doc_version,
last_updated=data["last_updated"],
)
sitemap = _build_sitemap(data["siblings"])

mirror.write_text(frontmatter + body.rstrip("\n") + "\n" + sitemap, encoding="utf-8")


def _build_frontmatter(title, description, doc_version, last_updated):
"""A YAML frontmatter block with the four agent-readability keys.

Values are JSON-encoded so colons, quotes, and unicode stay valid YAML
(JSON is a YAML subset for scalar strings).
"""
lines = ["---"]
lines.append("title: " + json.dumps(title, ensure_ascii=False))
lines.append("description: " + json.dumps(description, ensure_ascii=False))
lines.append("doc_version: " + json.dumps(str(doc_version), ensure_ascii=False))
lines.append("last_updated: " + json.dumps(str(last_updated), ensure_ascii=False))
lines.append("---")
lines.append("")
return "\n".join(lines) + "\n"


def _build_sitemap(siblings):
"""A ## Sitemap section linking sibling pages' markdown mirrors."""
lines = ["", "## Sitemap", ""]
if siblings:
for title, href in siblings:
label = title or href
lines.append(f"- [{label}]({href})")
else:
lines.append("- [Documentation home](/docs/)")
lines.append("")
return "\n".join(lines)


def _relative_mirror_href(from_url, to_url):
"""Relative href from one page's mirror to another page's .md mirror.

Both mirrors sit at <url>index.md. From "a/b/" to "c/d/" the relative path
from a/b/index.md to c/d/index.md is computed against the page directories.
"""
from_dir = from_url.rstrip("/")
to_dir = to_url.rstrip("/")
target = (to_dir + "/index.md") if to_dir else "index.md"
rel = os.path.relpath(target, start=from_dir if from_dir else ".")
return rel
26 changes: 26 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ theme:

plugins:
- search
- git-revision-date-localized:
enable_creation_date: false
type: iso_datetime
# A page with no git history yet (a newly added file) uses the build date
# rather than aborting the strict build with a "no git logs" warning.
fallback_to_build_date: true
# Serial processing: the parallel default needs semaphore creation,
# which is blocked under the sandboxed build environment. Serial cost
# is negligible for this doc set.
enable_parallel_processing: false
- llmstxt:
full_output: llms-full.txt
markdown_description: >-
Expand Down Expand Up @@ -90,9 +100,20 @@ nav:
- Reference:
- Command reference: reference/command-reference.md
- Frontmatter contract: reference/frontmatter-contract.md
- Glossary: reference/glossary.md
- Supported sandboxes: reference/sandbox.md
- Contributing: https://github.com/spacedock-dev/spacedock/blob/main/CONTRIBUTING.md

hooks:
# Enriches the per-page .md mirrors emitted by mkdocs-llmstxt with YAML
# frontmatter and a ## Sitemap section (agent-readability markdown.frontmatter
# and markdown.sitemap-section). Runs in on_post_build after llmstxt writes.
- hooks/enrich_md_mirrors.py

extra:
# Stamped into the per-page .md-mirror frontmatter (doc_version). Bump on release.
doc_version: 0.20.2

extra_css:
- stylesheets/tokens.css
- stylesheets/brand.css
Expand All @@ -101,6 +122,11 @@ markdown_extensions:
- admonition
- pymdownx.highlight:
anchor_linenums: true
# Emit language-* classes on <code> (agent-readability code.language-tags).
# Pygments highlights inline via <span> and drops the language-* class;
# disabling it renders <code class="language-*"> instead of server-side
# syntax coloring.
use_pygments: false
- pymdownx.details
- pymdownx.superfences:
custom_fences:
Expand Down
39 changes: 39 additions & 0 deletions overrides/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,43 @@
{{ super() }}
<link rel="preconnect" href="https://fonts.bunny.net" crossorigin>
<link rel="stylesheet" href="https://fonts.bunny.net/css2?family=hanken-grotesk:ital,wght@0,300;0,400;0,700;1,300;1,400;1,700&family=jetbrains-mono:ital,wght@0,400;0,700;1,400;1,700&display=fallback">
{#-
Agent-readability (a14y scorecard 0.2.0): inject Open Graph, JSON-LD
(TechArticle + BreadcrumbList + dateModified), and a markdown alternate-link
into every content page's <head>, mirroring the landing page's Astro layout.
The {% if page %} guard is load-bearing: Material renders extrahead for the
404 page with page = None, and an unguarded page.* reference fails
`mkdocs build --strict` with "'None' has no attribute 'meta'".
-#}
{% if page %}
<meta property="og:title" content="{{ page.title | striptags }} - {{ config.site_name }}">
<meta property="og:description" content="{{ page.meta.description | default(config.site_description) }}">
<meta property="og:type" content="article">
<meta property="og:url" content="{{ page.canonical_url }}">
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "TechArticle",
"headline": {{ (page.title | striptags) | tojson }},
"description": {{ (page.meta.description | default(config.site_description)) | tojson }},
"url": {{ page.canonical_url | tojson }},
"dateModified": {{ (page.meta.git_revision_date_localized_raw_iso_datetime | default('')) | tojson }},
"isPartOf": { "@type": "WebSite", "name": {{ config.site_name | tojson }}, "url": {{ config.site_url | tojson }} }
},
{
"@type": "BreadcrumbList",
"itemListElement": [
{% for anc in page.ancestors | reverse %}
{ "@type": "ListItem", "position": {{ loop.index }}, "name": {{ anc.title | striptags | tojson }} },
{% endfor %}
{ "@type": "ListItem", "position": {{ (page.ancestors | length) + 1 }}, "name": {{ page.title | striptags | tojson }}, "item": {{ page.canonical_url | tojson }} }
]
}
]
}
</script>
<link rel="alternate" type="text/markdown" href="{{ page.canonical_url }}index.md">
{% endif %}
{% endblock %}
1 change: 1 addition & 0 deletions overrides/partials/footer.html
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
{% include "partials/copyright.html" %}
<a class="sd-footer-glossary" href="{{ 'reference/glossary/' | url }}">Glossary</a>
<a class="sd-footer-llms" href="{{ 'llms.txt' | url }}">For agents: llms.txt</a>
{% if config.extra.social %}
{% include "partials/social.html" %}
Expand Down
Loading