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
20 changes: 20 additions & 0 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "ariel",
"interface": {
"displayName": "Ariel"
},
"plugins": [
{
"name": "ariel",
"source": {
"source": "local",
"path": "./"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ariel",
"version": "0.2.0",
"version": "0.3.0",
"description": "Generate step-by-step Mermaid diagram walkthroughs (HTML/SVG/MP4) from a YAML DSL. Bundles the ariel CLI (built from source on first use) and teaches agents to author walkthroughs.",
"author": {
"name": "Scott Rogowski",
Expand Down
34 changes: 34 additions & 0 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "ariel",
"version": "0.3.0",
"description": "Generate step-by-step Mermaid diagram walkthroughs (HTML/SVG/MP4) from a YAML DSL. Bundles the ariel CLI (built from source on first use) and teaches agents to author walkthroughs.",
"author": {
"name": "Scott Rogowski",
"url": "https://github.com/scottrogowski"
},
"homepage": "https://github.com/scottrogowski/ariel",
"repository": "https://github.com/scottrogowski/ariel",
"license": "MIT",
"keywords": [
"mermaid",
"diagram",
"walkthrough",
"visualization",
"documentation"
],
"skills": "./skills/",
"interface": {
"displayName": "Ariel",
"shortDescription": "Create guided Mermaid diagram walkthroughs.",
"longDescription": "Ariel teaches Codex to create and render narrated Mermaid diagram walkthroughs.",
"developerName": "Scott Rogowski",
"category": "Productivity",
"capabilities": [
"Command-line",
"Visualization"
],
"defaultPrompt": [
"Create an Ariel walkthrough for this system."
]
}
}
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ jobs:
- name: Validate release version
id: version
run: |
release_tag="$(python3 tools/ci/version_check.py)"
release_tag="$(python3 dev-tools/ci/version_check.py)"
echo "tag=$release_tag" >> "$GITHUB_OUTPUT"
echo "version=${release_tag#v}" >> "$GITHUB_OUTPUT"
- name: Lint
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/version-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
with:
go-version-file: go.mod
- name: Validate release version
run: python3 tools/ci/version_check.py
run: python3 dev-tools/ci/version_check.py
- name: Lint
run: make lint
- name: Test
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Read [spec/ariel-spec.md](spec/ariel-spec.md) before making changes.
1 change: 1 addition & 0 deletions CLAUDE.md
15 changes: 7 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,24 @@ build:
go build -o $(BIN) ./cmd/ariel

reconcile:
python3 tools/reconcile/reconcile.py
python3 dev-tools/reconcile/reconcile.py

examples: examples-ci
$(BIN) generate --format svg --output examples/example-output/ariel-why-output.svg examples/example-input/ariel-why.ariel.yaml
$(BIN) generate --format mp4 --output examples/example-output/ariel-why-output.mp4 examples/example-input/ariel-why.ariel.yaml
$(BIN) generate --theme light --format svg --output examples/example-output/ariel-what-output.svg examples/example-input/ariel-what.ariel.yaml
$(BIN) generate --theme light --format mp4 --output examples/example-output/ariel-what-output.mp4 examples/example-input/ariel-what.ariel.yaml
$(BIN) generate --format svg --output examples/example-output/ariel-what-output.svg examples/example-input/ariel-what.ariel.yaml
$(BIN) generate --format mp4 --output examples/example-output/ariel-what-output.mp4 examples/example-input/ariel-what.ariel.yaml

examples-ci: build
$(BIN) generate --output examples/example-output/ariel-why-output.html examples/example-input/ariel-why.ariel.yaml
# ariel-what renders with --theme light to showcase the light palette alongside ariel-why's default.
# Its section 3 step 8 (live reload loop) intentionally triggers a connectivity warning —
# FSWatch and Parse are shown together to illustrate the reload cycle.
$(BIN) generate --theme light --output examples/example-output/ariel-what-output.html examples/example-input/ariel-what.ariel.yaml
$(BIN) generate --output examples/example-output/ariel-what-output.html examples/example-input/ariel-what.ariel.yaml

test:
go test $(if $(GO_TEST_SKIP),-skip='$(GO_TEST_SKIP)') ./...
python3 -m unittest discover -s tools/ci -p '*_test.py'
python3 -m unittest discover -s tools/reconcile -p '*_test.py'
python3 -m unittest discover -s dev-tools/ci -p '*_test.py'
python3 -m unittest discover -s dev-tools/reconcile -p '*_test.py'
@echo ""
@echo "Tests pass. Run 'make examples' and inspect HTML/MP4 to validate visual output."

Expand All @@ -32,4 +31,4 @@ test-ci:

lint:
go vet ./...
python3 -m compileall -q tools
python3 -m compileall -q dev-tools
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ _If a picture is worth 1000 words, is a walkthrough worth 10,000?_

## Example SVGs

[![ariel-why walkthrough](examples/example-output/ariel-why-output.svg)](examples/example-output/ariel-why-output.svg)
[![ariel-why walkthrough](https://raw.githubusercontent.com/scottrogowski/ariel/refs/heads/main/examples/example-output/ariel-why-output.svg)](https://raw.githubusercontent.com/scottrogowski/ariel/refs/heads/main/examples/example-output/ariel-why-output.svg)

[![ariel-what walkthrough](examples/example-output/ariel-what-output.svg)](examples/example-output/ariel-what-output.svg)
[![ariel-what walkthrough](https://raw.githubusercontent.com/scottrogowski/ariel/refs/heads/main/examples/example-output/ariel-what-output.svg)](https://raw.githubusercontent.com/scottrogowski/ariel/refs/heads/main/examples/example-output/ariel-what-output.svg)

## Install

Expand All @@ -27,6 +27,13 @@ Gives your agent the `ariel` command and teaches it the workflow in one step:
/plugin install ariel
```

**Codex plugin (recommended)**

```sh
codex plugin marketplace add scottrogowski/ariel
codex plugin add ariel@ariel
```

**Go install**
```sh
go install github.com/scottrogowski/ariel/cmd/ariel@latest
Expand Down Expand Up @@ -72,7 +79,7 @@ Ariel follows spec-driven-development. Read spec/ariel-spec.md. If you are an ag

After every code change that could alter the output rendering (which is almost every code change), run `make examples`. This is also NOT optional. Almost every commit should have modifications to the examples.

Set `.claude-plugin/plugin.json` to the next release version in every pull request. Minor releases add real features. Patch releases contain only bug fixes or aesthetic updates. Merging to `main` publishes the matching Git tag, GitHub Release, and binaries.
Set `.claude-plugin/plugin.json` to the next release version in every pull request. `make reconcile` copies it to the Codex manifest. Minor releases add features. Patch releases contain only fixes or aesthetic updates. Merging to `main` publishes the matching Git tag, GitHub Release, and binaries.

## Testing

Expand Down
5 changes: 2 additions & 3 deletions bin/ariel
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
#!/bin/sh
# Plugin wrapper: builds the ariel CLI from the bundled source on first use, then
# execs it. Resolves the plugin root from this script's own path so it does not
# depend on ${CLAUDE_PLUGIN_ROOT} being exported to bin/ commands.
# Plugin launcher: builds the bundled ariel CLI on first use, then executes it.
# The script resolves the plugin root without depending on the current directory.
set -e

script_dir=$(cd "$(dirname "$0")" && pwd)
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
131 changes: 131 additions & 0 deletions dev-tools/reconcile/reconcile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Reconcile generated repository files with their authored sources."""

from __future__ import annotations

import json
from pathlib import Path
import sys

REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
GUIDE_PATH = REPOSITORY_ROOT / "internal" / "guide" / "guide.txt"
SKILL_PATH = REPOSITORY_ROOT / "skills" / "create-walkthrough" / "SKILL.md"
CLAUDE_MANIFEST_PATH = REPOSITORY_ROOT / ".claude-plugin" / "plugin.json"
CODEX_MANIFEST_PATH = REPOSITORY_ROOT / ".codex-plugin" / "plugin.json"
CODEX_MARKETPLACE_PATH = REPOSITORY_ROOT / ".agents" / "plugins" / "marketplace.json"
BEGIN_MARKER = "<!-- BEGIN GENERATED: ariel guide — regenerate with `make reconcile`; do not edit by hand -->"
END_MARKER = "<!-- END GENERATED: ariel guide -->"
CODEX_DISPLAY_NAME = "Ariel"
CODEX_CATEGORY = "Productivity"
SHARED_MANIFEST_FIELDS = (
"name",
"version",
"description",
"author",
"homepage",
"repository",
"license",
"keywords",
)


def render_block(guide: str) -> str:
"""Render the generated Markdown block."""
return f"{BEGIN_MARKER}\n\n```\n{guide.rstrip()}\n```\n\n{END_MARKER}"


def replace_block(skill: str, generated_block: str) -> str:
"""Replace the marker-delimited block in a skill document."""
start = skill.find(BEGIN_MARKER)
if start < 0:
raise ValueError(f"begin marker not found: {BEGIN_MARKER!r}")
end = skill.find(END_MARKER, start)
if end < 0:
raise ValueError(f"end marker not found: {END_MARKER!r}")
end += len(END_MARKER)
return skill[:start] + generated_block + skill[end:]


def read_claude_manifest() -> dict[str, object]:
"""Read shared plugin metadata from the authored manifest."""
manifest = json.loads(CLAUDE_MANIFEST_PATH.read_text(encoding="utf-8"))
if not isinstance(manifest, dict):
raise ValueError(f"{CLAUDE_MANIFEST_PATH} must contain an object")
missing_fields = [field for field in SHARED_MANIFEST_FIELDS if field not in manifest]
if missing_fields:
missing = ", ".join(missing_fields)
raise ValueError(f"{CLAUDE_MANIFEST_PATH} lacks required fields: {missing}")
return manifest


def render_codex_manifest(claude_manifest: dict[str, object]) -> str:
"""Render the generated Codex plugin manifest."""
author = claude_manifest["author"]
if not isinstance(author, dict) or not isinstance(author.get("name"), str):
raise ValueError(f"{CLAUDE_MANIFEST_PATH} has no author name")
manifest = {field: claude_manifest[field] for field in SHARED_MANIFEST_FIELDS}
manifest.update(
{
"skills": "./skills/",
"interface": {
"displayName": CODEX_DISPLAY_NAME,
"shortDescription": "Create guided Mermaid diagram walkthroughs.",
"longDescription": "Ariel teaches Codex to create and render narrated Mermaid diagram walkthroughs.",
"developerName": author["name"],
"category": CODEX_CATEGORY,
"capabilities": ["Command-line", "Visualization"],
"defaultPrompt": ["Create an Ariel walkthrough for this system."],
},
}
)
return json.dumps(manifest, indent=2) + "\n"


def render_codex_marketplace(claude_manifest: dict[str, object]) -> str:
"""Render the generated Codex marketplace."""
plugin_name = claude_manifest["name"]
if not isinstance(plugin_name, str):
raise ValueError(f"{CLAUDE_MANIFEST_PATH} has no plugin name")
marketplace = {
"name": plugin_name,
"interface": {"displayName": CODEX_DISPLAY_NAME},
"plugins": [
{
"name": plugin_name,
"source": {"source": "local", "path": "./"},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL",
},
"category": CODEX_CATEGORY,
}
],
}
return json.dumps(marketplace, indent=2) + "\n"


def reconcile() -> None:
"""Update every generated repository file."""
guide = GUIDE_PATH.read_text(encoding="utf-8")
skill = SKILL_PATH.read_text(encoding="utf-8")
updated = replace_block(skill, render_block(guide))
SKILL_PATH.write_text(updated, encoding="utf-8")
claude_manifest = read_claude_manifest()
codex_manifest = render_codex_manifest(claude_manifest)
CODEX_MANIFEST_PATH.write_text(codex_manifest, encoding="utf-8")
codex_marketplace = render_codex_marketplace(claude_manifest)
CODEX_MARKETPLACE_PATH.write_text(codex_marketplace, encoding="utf-8")


def main() -> int:
"""Reconcile generated files or return a failing exit code."""
try:
reconcile()
except (OSError, ValueError) as error:
print(f"reconcile: {error}", file=sys.stderr)
return 1
print("reconciled generated repository files")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ def test_skill_contains_current_guide(self) -> None:
expected = reconcile.render_block(guide)
self.assertIn(expected, skill)

# Prevent shared metadata from drifting between agent plugin manifests.
def test_codex_manifest_matches_claude_manifest(self) -> None:
expected = reconcile.render_codex_manifest(reconcile.read_claude_manifest())
actual = reconcile.CODEX_MANIFEST_PATH.read_text(encoding="utf-8")
self.assertEqual(actual, expected)

# Prevent marketplace metadata from drifting from the plugin manifest.
def test_codex_marketplace_matches_claude_manifest(self) -> None:
expected = reconcile.render_codex_marketplace(reconcile.read_claude_manifest())
actual = reconcile.CODEX_MARKETPLACE_PATH.read_text(encoding="utf-8")
self.assertEqual(actual, expected)

def test_replace_block_rejects_missing_markers(self) -> None:
with self.assertRaises(ValueError):
reconcile.replace_block("unmanaged content", "generated content")
Expand Down
Loading
Loading