diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json
deleted file mode 100644
index f7e0fac10..000000000
--- a/.agents/plugins/marketplace.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "name": "gaia-marketplace",
- "interface": {
- "displayName": "Gaia"
- },
- "plugins": [
- {
- "name": "gaia",
- "source": "./skills/",
- "policy": {
- "installation": "AVAILABLE",
- "authentication": "ON_INSTALL"
- }
- }
- ]
-}
diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
deleted file mode 100644
index 92f4b9ef7..000000000
--- a/.claude-plugin/marketplace.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "name": "gaia-marketplace",
- "owner": {
- "name": "SiliconEinstein"
- },
- "plugins": [
- {
- "name": "gaia",
- "source": "./",
- "description": "Gaia knowledge formalization skills",
- "version": "0.4.3"
- }
- ]
-}
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
deleted file mode 100644
index fde0c74d9..000000000
--- a/.claude-plugin/plugin.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "name": "gaia",
- "version": "0.4.3",
- "description": "Gaia knowledge formalization skills — convert scientific papers, textbooks, or any knowledge source into Gaia knowledge packages.",
- "author": {
- "name": "SiliconEinstein"
- },
- "repository": "https://github.com/SiliconEinstein/Gaia",
- "license": "MIT",
- "keywords": ["gaia", "knowledge", "formalization", "science", "reasoning"]
-}
diff --git a/.claude/projects/-Users-kunchen-project-Gaia/memory/bug_formalexpr_relation_prior.md b/.claude/projects/-Users-kunchen-project-Gaia/memory/bug_formalexpr_relation_prior.md
deleted file mode 100644
index 985e0d3b3..000000000
--- a/.claude/projects/-Users-kunchen-project-Gaia/memory/bug_formalexpr_relation_prior.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-name: FormalExpr relation conclusion prior bug
-description: FormalExpr internal relation operator conclusions get π=0.5 instead of 1-ε, causing dead-end constraint vanishing in elimination/case_analysis
-type: project
----
-
-FormalExpr expand path in `gaia/bp/lowering.py:232-237` uses `_ensure_claim_var` (default π=0.5) for ALL operator conclusions, including relation operators. Top-level operators (line 81-106) correctly distinguish relation vs directed, but FormalExpr path does not.
-
-**Why:** This causes dead-end relation conclusions (Eq, Contra in elimination; Eq in case_analysis) to lose their constraint — the exact bug #340 describes.
-
-**How to apply:** Fix by adding relation-type prior logic to the FormalExpr expand path. The prior should be 1-ε for relation operator conclusions, 0.5 for directed operator conclusions, matching the top-level operator path.
-
-**Files:** `gaia/bp/lowering.py` lines 232-237 (FormalExpr expand), line 126-131 (`_ensure_claim_var`).
diff --git a/.claude/settings.json b/.claude/settings.json
deleted file mode 100644
index 34b0215b5..000000000
--- a/.claude/settings.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "enabledPlugins": {
- "codex@openai-codex": true
- }
-}
diff --git a/.claude/skills/brainstorming/SKILL.md b/.claude/skills/brainstorming/SKILL.md
deleted file mode 100644
index 54d7d384e..000000000
--- a/.claude/skills/brainstorming/SKILL.md
+++ /dev/null
@@ -1,147 +0,0 @@
----
-name: brainstorming
-description: "You MUST use this before any creative work - creating features, building components, adding functionality, or modifying behavior. Explores user intent, requirements and design before implementation."
----
-
-# Brainstorming Ideas Into Designs
-
-Help turn ideas into fully formed designs and specs through natural collaborative dialogue.
-
-Start by understanding the current project context, then ask questions one at a time to refine the idea. Once you understand what you're building, present the design and get user approval.
-
-
-Do NOT invoke any implementation skill, write any code, scaffold any project, or take any implementation action until you have presented a design and the user has approved it. This applies to EVERY project regardless of perceived simplicity.
-
-
-## Anti-Pattern: "This Is Too Simple To Need A Design"
-
-Every project goes through this process. A todo list, a single-function utility, a config change — all of them. "Simple" projects are where unexamined assumptions cause the most wasted work. The design can be short (a few sentences for truly simple projects), but you MUST present it and get approval.
-
-## Checklist
-
-You MUST create a task for each of these items and complete them in order:
-
-1. **Explore project context** — check files, docs, recent commits
-2. **Offer visual companion** (if topic will involve visual questions) — this is its own message, not combined with a clarifying question. See the Visual Companion section below.
-3. **Ask clarifying questions** — one at a time, understand purpose/constraints/success criteria
-4. **Propose 2-3 approaches** — with trade-offs and your recommendation
-5. **Present design** — in sections scaled to their complexity, get user approval after each section
-6. **Write design doc** — save to `docs/specs/YYYY-MM-DD--design.md` and commit
-7. **Transition to implementation** — invoke writing-plans skill to create implementation plan
-
-## Process Flow
-
-```dot
-digraph brainstorming {
- "Explore project context" [shape=box];
- "Visual questions ahead?" [shape=diamond];
- "Offer Visual Companion\n(own message, no other content)" [shape=box];
- "Ask clarifying questions" [shape=box];
- "Propose 2-3 approaches" [shape=box];
- "Present design sections" [shape=box];
- "User approves design?" [shape=diamond];
- "Write design doc" [shape=box];
- "Invoke writing-plans skill" [shape=doublecircle];
-
- "Explore project context" -> "Visual questions ahead?";
- "Visual questions ahead?" -> "Offer Visual Companion\n(own message, no other content)" [label="yes"];
- "Visual questions ahead?" -> "Ask clarifying questions" [label="no"];
- "Offer Visual Companion\n(own message, no other content)" -> "Ask clarifying questions";
- "Ask clarifying questions" -> "Propose 2-3 approaches";
- "Propose 2-3 approaches" -> "Present design sections";
- "Present design sections" -> "User approves design?";
- "User approves design?" -> "Present design sections" [label="no, revise"];
- "User approves design?" -> "Write design doc" [label="yes"];
- "Write design doc" -> "Invoke writing-plans skill";
-}
-```
-
-**The terminal state is invoking writing-plans.** Do NOT invoke frontend-design, mcp-builder, or any other implementation skill. The ONLY skill you invoke after brainstorming is writing-plans.
-
-## The Process
-
-**Understanding the idea:**
-
-- Check out the current project state first (files, docs, recent commits)
-- Before asking detailed questions, assess scope: if the request describes multiple independent subsystems (e.g., "build a platform with chat, file storage, billing, and analytics"), flag this immediately. Don't spend questions refining details of a project that needs to be decomposed first.
-- If the project is too large for a single spec, help the user decompose into sub-projects: what are the independent pieces, how do they relate, what order should they be built? Then brainstorm the first sub-project through the normal design flow. Each sub-project gets its own spec → plan → implementation cycle.
-- For appropriately-scoped projects, ask questions one at a time to refine the idea
-- Prefer multiple choice questions when possible, but open-ended is fine too
-- Only one question per message - if a topic needs more exploration, break it into multiple questions
-- Focus on understanding: purpose, constraints, success criteria
-
-**Exploring approaches:**
-
-- Propose 2-3 different approaches with trade-offs
-- Present options conversationally with your recommendation and reasoning
-- Lead with your recommended option and explain why
-
-**Presenting the design:**
-
-- Once you believe you understand what you're building, present the design
-- Scale each section to its complexity: a few sentences if straightforward, up to 200-300 words if nuanced
-- Ask after each section whether it looks right so far
-- Cover: architecture, components, data flow, error handling, testing
-- Be ready to go back and clarify if something doesn't make sense
-
-**Design for isolation and clarity:**
-
-- Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently
-- For each unit, you should be able to answer: what does it do, how do you use it, and what does it depend on?
-- Can someone understand what a unit does without reading its internals? Can you change the internals without breaking consumers? If not, the boundaries need work.
-- Smaller, well-bounded units are also easier for you to work with - you reason better about code you can hold in context at once, and your edits are more reliable when files are focused. When a file grows large, that's often a signal that it's doing too much.
-
-**Working in existing codebases:**
-
-- Explore the current structure before proposing changes. Follow existing patterns.
-- Where existing code has problems that affect the work (e.g., a file that's grown too large, unclear boundaries, tangled responsibilities), include targeted improvements as part of the design - the way a good developer improves code they're working in.
-- Don't propose unrelated refactoring. Stay focused on what serves the current goal.
-
-## After the Design
-
-**Documentation:**
-
-- Write the validated design (spec) to `docs/specs/YYYY-MM-DD--design.md`
- - (User preferences for spec location override this default)
-- Use elements-of-style:writing-clearly-and-concisely skill if available
-- Commit the design document to git
-
-**Spec Review Loop:**
-After writing the spec document:
-
-1. Dispatch spec-document-reviewer subagent (see spec-document-reviewer-prompt.md)
-2. If Issues Found: fix, re-dispatch, repeat until Approved
-3. If loop exceeds 5 iterations, surface to human for guidance
-
-**Implementation:**
-
-- Invoke the writing-plans skill to create a detailed implementation plan
-- Do NOT invoke any other skill. writing-plans is the next step.
-
-## Key Principles
-
-- **One question at a time** - Don't overwhelm with multiple questions
-- **Multiple choice preferred** - Easier to answer than open-ended when possible
-- **YAGNI ruthlessly** - Remove unnecessary features from all designs
-- **Explore alternatives** - Always propose 2-3 approaches before settling
-- **Incremental validation** - Present design, get approval before moving on
-- **Be flexible** - Go back and clarify when something doesn't make sense
-
-## Visual Companion
-
-A browser-based companion for showing mockups, diagrams, and visual options during brainstorming. Available as a tool — not a mode. Accepting the companion means it's available for questions that benefit from visual treatment; it does NOT mean every question goes through the browser.
-
-**Offering the companion:** When you anticipate that upcoming questions will involve visual content (mockups, layouts, diagrams), offer it once for consent:
-> "Some of what we're working on might be easier to explain if I can show it to you in a web browser. I can put together mockups, diagrams, comparisons, and other visuals as we go. This feature is still new and can be token-intensive. Want to try it? (Requires opening a local URL)"
-
-**This offer MUST be its own message.** Do not combine it with clarifying questions, context summaries, or any other content. The message should contain ONLY the offer above and nothing else. Wait for the user's response before continuing. If they decline, proceed with text-only brainstorming.
-
-**Per-question decision:** Even after the user accepts, decide FOR EACH QUESTION whether to use the browser or the terminal. The test: **would the user understand this better by seeing it than reading it?**
-
-- **Use the browser** for content that IS visual — mockups, wireframes, layout comparisons, architecture diagrams, side-by-side visual designs
-- **Use the terminal** for content that is text — requirements questions, conceptual choices, tradeoff lists, A/B/C/D text options, scope decisions
-
-A question about a UI topic is not automatically a visual question. "What does personality mean in this context?" is a conceptual question — use the terminal. "Which wizard layout works better?" is a visual question — use the browser.
-
-If they agree to the companion, read the detailed guide before proceeding:
-`skills/brainstorming/visual-companion.md`
diff --git a/.claude/skills/brainstorming/spec-document-reviewer-prompt.md b/.claude/skills/brainstorming/spec-document-reviewer-prompt.md
deleted file mode 100644
index d693d2f46..000000000
--- a/.claude/skills/brainstorming/spec-document-reviewer-prompt.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# Spec Document Reviewer Prompt Template
-
-Use this template when dispatching a spec document reviewer subagent.
-
-**Purpose:** Verify the spec is complete, consistent, and ready for implementation planning.
-
-**Dispatch after:** Spec document is written to docs/specs/
-
-```
-Task tool (general-purpose):
- description: "Review spec document"
- prompt: |
- You are a spec document reviewer. Verify this spec is complete and ready for planning.
-
- **Spec to review:** [SPEC_FILE_PATH]
-
- ## What to Check
-
- | Category | What to Look For |
- |----------|------------------|
- | Completeness | TODOs, placeholders, "TBD", incomplete sections |
- | Coverage | Missing error handling, edge cases, integration points |
- | Consistency | Internal contradictions, conflicting requirements |
- | Clarity | Ambiguous requirements |
- | YAGNI | Unrequested features, over-engineering |
- | Scope | Focused enough for a single plan — not covering multiple independent subsystems |
- | Architecture | Units with clear boundaries, well-defined interfaces, independently understandable and testable |
-
- ## CRITICAL
-
- Look especially hard for:
- - Any TODO markers or placeholder text
- - Sections saying "to be defined later" or "will spec when X is done"
- - Sections noticeably less detailed than others
- - Units that lack clear boundaries or interfaces — can you understand what each unit does without reading its internals?
-
- ## Output Format
-
- ## Spec Review
-
- **Status:** ✅ Approved | ❌ Issues Found
-
- **Issues (if any):**
- - [Section X]: [specific issue] - [why it matters]
-
- **Recommendations (advisory):**
- - [suggestions that don't block approval]
-```
-
-**Reviewer returns:** Status, Issues (if any), Recommendations
diff --git a/.claude/skills/brainstorming/visual-companion.md b/.claude/skills/brainstorming/visual-companion.md
deleted file mode 100644
index 62e15a723..000000000
--- a/.claude/skills/brainstorming/visual-companion.md
+++ /dev/null
@@ -1,260 +0,0 @@
-# Visual Companion Guide
-
-Browser-based visual brainstorming companion for showing mockups, diagrams, and options.
-
-## When to Use
-
-Decide per-question, not per-session. The test: **would the user understand this better by seeing it than reading it?**
-
-**Use the browser** when the content itself is visual:
-
-- **UI mockups** — wireframes, layouts, navigation structures, component designs
-- **Architecture diagrams** — system components, data flow, relationship maps
-- **Side-by-side visual comparisons** — comparing two layouts, two color schemes, two design directions
-- **Design polish** — when the question is about look and feel, spacing, visual hierarchy
-- **Spatial relationships** — state machines, flowcharts, entity relationships rendered as diagrams
-
-**Use the terminal** when the content is text or tabular:
-
-- **Requirements and scope questions** — "what does X mean?", "which features are in scope?"
-- **Conceptual A/B/C choices** — picking between approaches described in words
-- **Tradeoff lists** — pros/cons, comparison tables
-- **Technical decisions** — API design, data modeling, architectural approach selection
-- **Clarifying questions** — anything where the answer is words, not a visual preference
-
-A question *about* a UI topic is not automatically a visual question. "What kind of wizard do you want?" is conceptual — use the terminal. "Which of these wizard layouts feels right?" is visual — use the browser.
-
-## How It Works
-
-The server watches a directory for HTML files and serves the newest one to the browser. You write HTML content, the user sees it in their browser and can click to select options. Selections are recorded to a `.events` file that you read on your next turn.
-
-**Content fragments vs full documents:** If your HTML file starts with `
-
-
Continuing in terminal...
-
- ```
-
- This prevents the user from staring at a resolved choice while the conversation has moved on. When the next visual question comes up, push a new content file as usual.
-
-6. Repeat until done.
-
-## Writing Content Fragments
-
-Write just the content that goes inside the page. The server wraps it in the frame template automatically (header, theme CSS, selection indicator, and all interactive infrastructure).
-
-**Minimal example:**
-
-```html
-
Which layout works better?
-
Consider readability and visual hierarchy
-
-
-
-
A
-
-
Single Column
-
Clean, focused reading experience
-
-
-
-
B
-
-
Two Column
-
Sidebar navigation with main content
-
-
-
-```
-
-That's it. No ``, no CSS, no `"
+ return template.replace(GRAPH_DATA_PLACEHOLDER, injection, 1)
+
+
+def _render_svg(dot_source: str, *, theme: str) -> str:
+ """Render *dot_source* to SVG via the appropriate Graphviz binary.
+
+ For ``stellaris`` / ``dark`` the resulting SVG is post-processed to inject
+ the ```` glow filter block and recolour the canvas background — see
+ :mod:`gaia.cli.commands._stellaris_svg`.
+
+ Raises:
+ GaiaPackagingError: when the required Graphviz binary is missing from
+ ``PATH``, or when it exits non-zero.
+ """
+ binary = _SVG_LAYOUT_BINARY[theme]
+ binary_path = shutil.which(binary)
+ if binary_path is None:
+ raise GaiaPackagingError(
+ f"Error: Graphviz `{binary}` binary not found on PATH. Install Graphviz "
+ "first (`apt install graphviz` / `brew install graphviz`) and retry. "
+ "Alternatively, emit the dot source with `--format dot` and render "
+ "it manually."
+ )
+ try:
+ proc = subprocess.run(
+ [binary_path, "-Tsvg"],
+ input=dot_source,
+ capture_output=True,
+ text=True,
+ check=False,
+ )
+ except OSError as exc:
+ raise GaiaPackagingError(f"Error: failed to invoke Graphviz `{binary}`: {exc}") from exc
+ if proc.returncode != 0:
+ stderr = (proc.stderr or "").strip()
+ raise GaiaPackagingError(
+ f"Error: Graphviz `{binary}` exited with code {proc.returncode}."
+ + (f"\n stderr: {stderr}" if stderr else "")
+ )
+ svg = proc.stdout
+ if theme in ("stellaris", "dark"):
+ svg = post_process_stellaris_svg(svg)
+ return svg
+
+
+def _validate_starmap_options(fmt: str, theme: str) -> None:
+ """Validate `gaia inspect starmap` format and theme options."""
+ if fmt not in _DEFAULT_OUT:
+ typer.echo(
+ f"Error: --format must be one of {sorted(_DEFAULT_OUT)}; got {fmt!r}.",
+ err=True,
+ )
+ raise typer.Exit(2)
+
+ if theme not in _VALID_THEMES:
+ typer.echo(
+ f"Error: --theme must be one of {sorted(_VALID_THEMES)}; got {theme!r}.",
+ err=True,
+ )
+ raise typer.Exit(2)
+
+
+def _load_starmap_inputs(path: str) -> tuple[Any, Any]:
+ """Load and compile package inputs for starmap rendering."""
+ try:
+ loaded = load_gaia_package(path)
+ apply_package_priors(loaded)
+ compiled = compile_loaded_package_artifact(loaded)
+ except GaiaPackagingError as exc:
+ typer.echo(str(exc), err=True)
+ raise typer.Exit(1) from exc
+ return loaded, compiled
+
+
+def _emit_starmap_validation(compiled: Any) -> None:
+ """Validate compiled IR before rendering a starmap."""
+ graph_validation = validate_local_graph(compiled.graph)
+ for warning in graph_validation.warnings:
+ typer.echo(f"Warning: {warning}")
+ if graph_validation.errors:
+ for error in graph_validation.errors:
+ typer.echo(f"Error: {error}", err=True)
+ raise typer.Exit(1)
+
+
+def _require_starmap_artifacts_fresh(loaded: Any, compiled: Any, ir: dict[str, Any]) -> None:
+ """Require stored compile artifacts to match the in-memory compiled IR."""
+ gaia_dir = loaded.pkg_path / ".gaia"
+ ir_hash_path = gaia_dir / "ir_hash"
+ ir_json_path = gaia_dir / "ir.json"
+ if not ir_hash_path.exists() or not ir_json_path.exists():
+ typer.echo("Error: missing compiled artifacts; run `gaia build compile` first.", err=True)
+ raise typer.Exit(1)
+ if ir_hash_path.read_text().strip() != compiled.graph.ir_hash:
+ typer.echo("Error: compiled artifacts are stale; run `gaia build compile` again.", err=True)
+ raise typer.Exit(1)
+ try:
+ stored_ir = json.loads(ir_json_path.read_text())
+ except json.JSONDecodeError as exc:
+ typer.echo(f"Error: .gaia/ir.json is not valid JSON: {exc}", err=True)
+ raise typer.Exit(1) from exc
+ if stored_ir.get("ir_hash") != compiled.graph.ir_hash or stored_ir != ir:
+ typer.echo("Error: compiled artifacts are stale; run `gaia build compile` again.", err=True)
+ raise typer.Exit(1)
+
+
+def _load_starmap_beliefs(loaded: Any, compiled: Any) -> dict[str, Any] | None:
+ """Load optional beliefs.json and require freshness when present."""
+ beliefs_path = loaded.pkg_path / ".gaia" / "beliefs.json"
+ if not beliefs_path.exists():
+ return None
+ try:
+ beliefs_data = cast(dict[str, Any], json.loads(beliefs_path.read_text()))
+ except json.JSONDecodeError as exc:
+ typer.echo(f"Error: {beliefs_path} is not valid JSON: {exc}", err=True)
+ raise typer.Exit(1) from exc
+ if beliefs_data.get("ir_hash") != compiled.graph.ir_hash:
+ typer.echo(
+ "Error: beliefs are stale; run `gaia run infer` again.",
+ err=True,
+ )
+ raise typer.Exit(1)
+ return beliefs_data
+
+
+def _render_starmap_content(graph_json: str, *, fmt: str, theme: str) -> str:
+ """Render graph JSON into the requested starmap output format."""
+ if fmt == "html":
+ try:
+ return _render_html(_load_template(), graph_json)
+ except GaiaPackagingError as exc:
+ typer.echo(str(exc), err=True)
+ raise typer.Exit(1) from exc
+ if fmt == "svg":
+ dot_source = to_dot(graph_json, theme=theme)
+ try:
+ return _render_svg(dot_source, theme=theme)
+ except GaiaPackagingError as exc:
+ typer.echo(str(exc), err=True)
+ raise typer.Exit(1) from exc
+ return to_dot(graph_json, theme=theme)
+
+
+def starmap_command(
+ path: str = typer.Argument(".", help="Path to knowledge package directory"),
+ out: str = typer.Option(
+ None,
+ "--out",
+ help=(
+ "Output file. Defaults to '.gaia/starmap.html' (html) or "
+ "'.gaia/starmap.dot' (dot), relative to the package directory; "
+ "absolute paths are honored as-is."
+ ),
+ ),
+ fmt: str = typer.Option(
+ "html",
+ "--format",
+ help=(
+ "Output format: 'html' (interactive Sigma.js), 'dot' "
+ "(paper-ready Graphviz source), or 'svg' (rendered figure, "
+ "stellaris glow filters baked in)."
+ ),
+ ),
+ theme: str = typer.Option(
+ "light",
+ "--theme",
+ help=(
+ "Visual theme for 'dot' / 'svg' output. 'light' (default) is the "
+ "flat paper-friendly palette. 'stellaris' (alias: 'dark') is a "
+ "deep-space dark variant. For 'svg' the stellaris variant gets "
+ "an injected block with radial-gradient background and "
+ "glow filters bound to contradiction / support / root nodes."
+ ),
+ ),
+) -> None:
+ r"""Emit a starmap of the compiled package.
+
+ Three formats are supported:
+
+ * ``html`` (default) — single-file interactive Sigma.js visualization.
+ Double-click to open in a browser; no server required.
+ * ``dot`` — a Graphviz ``digraph`` source. Pipe through ``dot`` (Graphviz)
+ to get a paper-ready figure. ``graphviz`` must be installed separately
+ (``brew install graphviz`` / ``apt install graphviz``).
+ * ``svg`` — rendered figure, end-to-end. Internally calls ``dot``
+ (light theme) or ``sfdp`` (stellaris/dark) on the dot source, then for
+ the stellaris theme injects an SVG ```` block with a radial
+ gradient background and three glow filters keyed off ``class="..."``
+ markers (contradiction / support / root). Requires ``graphviz`` on
+ ``PATH``.
+
+ Compile freshness, beliefs freshness, and graph validation gates apply to
+ all formats.
+
+ Examples:
+ # Interactive HTML (default):
+ gaia inspect starmap path/to/pkg
+
+ # DOT source (manually pipe through dot/sfdp for full control):
+ gaia inspect starmap path/to/pkg --format dot --out figures/starmap.dot
+ dot -Tsvg figures/starmap.dot -o figures/starmap.svg
+
+ # End-to-end paper figure (light, no glow):
+ gaia inspect starmap path/to/pkg --format svg --out figures/starmap.svg
+
+ # End-to-end paper figure with stellaris glow defs baked in:
+ gaia inspect starmap path/to/pkg --format svg --theme stellaris \
+ --out figures/starmap_stellaris.svg
+
+ # PNG preview at higher DPI from the dot source:
+ dot -Tpng -Gdpi=200 figures/starmap.dot -o figures/starmap.png
+
+ # PDF for direct LaTeX \includegraphics inclusion:
+ dot -Tpdf figures/starmap.dot -o figures/starmap.pdf
+ """
+ _validate_starmap_options(fmt, theme)
+ loaded, compiled = _load_starmap_inputs(path)
+ _emit_starmap_validation(compiled)
+ ir = compiled.to_json()
+ _require_starmap_artifacts_fresh(loaded, compiled, ir)
+
+ # Beliefs are optional — degrade gracefully when absent. When present they
+ # MUST be fresh, mirroring `render`.
+ beliefs_data = _load_starmap_beliefs(loaded, compiled)
+ param_data = param_data_from_ir_metadata(ir)
+ exported_ids = {k["id"] for k in ir.get("knowledges", []) if k.get("exported")}
+
+ graph_json = generate_graph_json(
+ ir,
+ beliefs_data=beliefs_data,
+ param_data=param_data,
+ exported_ids=exported_ids,
+ )
+ graph_payload = json.loads(graph_json)
+ content = _render_starmap_content(graph_json, fmt=fmt, theme=theme)
+
+ out_path = Path(out) if out is not None else Path(_DEFAULT_OUT[fmt])
+ if not out_path.is_absolute():
+ out_path = loaded.pkg_path / out_path
+ out_path.parent.mkdir(parents=True, exist_ok=True)
+ out_path.write_text(content, encoding="utf-8")
+
+ node_count = len(graph_payload.get("nodes", []))
+ edge_count = len(graph_payload.get("edges", []))
+ typer.echo(f"Wrote starmap to {out_path} ({node_count} nodes, {edge_count} edges)")
diff --git a/gaia/cli/commands/starmap_replay.py b/gaia/cli/commands/starmap_replay.py
new file mode 100644
index 000000000..185773983
--- /dev/null
+++ b/gaia/cli/commands/starmap_replay.py
@@ -0,0 +1,416 @@
+"""gaia inspect starmap-replay v4 — IR-tick replay with pinned graphviz layout.
+
+Reads the two JSONL logs an ``lkm-to-gaia`` run leaves under a package's
+``artifacts/lkm-discovery/`` directory and renders a single self-contained
+HTML file that plays back the IR-side construction of the package.
+
+v4 contract (vs v3):
+
+* **Tick axis is per-``gaia_action``, not per-event.** Each entry of
+ ``event.gaia_actions`` whose ``action`` lands an IR change
+ (``claim``/``support``/``deduction``/``contradiction``/``equivalence``/
+ ``prior``) is one IR-tick. Events with no IR-relevant actions still
+ appear on the timeline as informational markers (``round_open``,
+ ``stage_transition``, retrievals, etc.) but contribute zero ticks.
+
+* **Pinned canonical layout.** The frontend gets a ``final_layout`` table
+ baked from ``dot -Tjson0`` against the same DOT source ``gaia inspect starmap
+ --format dot`` produces. Nodes are placed at their pinned coordinates
+ on first appearance; cluster boxes match ``_dot.py`` styling. This
+ command degrades gracefully when graphviz is missing or the package
+ has no compiled IR — replay still renders, with no pinned layout
+ (frontend falls back to a centred no-op).
+
+* **Per-round belief snapshots.** For each ``round_id`` seen in the
+ growth-log stream, a truncated IR (only knowledges introduced by
+ end-of-round R) is run through ``InferenceEngine`` and the resulting
+ beliefs are baked as ``round_beliefs``. The frontend animates each
+ claim node's belief number across round boundaries.
+
+The frontend half lives in ``viz/src/starmap-replay.ts`` (entry) and
+``viz/src/replay/*.ts``. The build pipeline (``cd viz && npm run
+build:replay``) inlines bundle + CSS into a single template HTML which
+is shipped at ``gaia/cli/starmap_replay_assets/template.html``; this
+command injects the timeline JSON into that template at the
+```` placeholder and writes the result.
+"""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+from typing import Any
+
+import typer
+
+from gaia.cli.commands._dot import to_dot
+from gaia.cli.commands._graph_json import generate_graph_json
+from gaia.cli.commands._render_priors import param_data_from_ir_metadata
+from gaia.cli.commands._replay_build import (
+ annotate_layout_with_kinds,
+ annotate_ticks_with_survival,
+ bridge_event_symbols_to_layout,
+ collect_round_order,
+ compute_dot_layout,
+ compute_round_beliefs,
+ rekey_layout_to_lkm_ids,
+ split_into_ir_ticks,
+ topo_reorder_ticks,
+)
+from gaia.engine.packaging import (
+ GaiaPackagingError,
+ apply_package_priors,
+ compile_loaded_package_artifact,
+ ensure_package_env,
+ load_gaia_package,
+)
+
+TIMELINE_PLACEHOLDER = ""
+DEFAULT_OUT_RELATIVE = ".gaia/starmap-replay.html"
+ARTIFACTS_SUBDIR = "artifacts/lkm-discovery"
+RETRIEVAL_LOG_NAME = "retrieval_log.jsonl"
+GROWTH_LOG_NAME = "graph_growth_log.jsonl"
+SCHEMA_VERSION = "1"
+
+
+def _load_template() -> str:
+ """Read the shipped placeholder HTML template."""
+ import gaia.cli.starmap_replay_assets as assets_pkg
+
+ template_path = Path(assets_pkg.__file__).parent / "template.html"
+ return template_path.read_text(encoding="utf-8")
+
+
+def _render_html(template: str, timeline_json: str) -> str:
+ """Inject the timeline JSON payload into *template* at the placeholder."""
+ if TIMELINE_PLACEHOLDER not in template:
+ raise RuntimeError(
+ f"Error: starmap-replay template is missing the {TIMELINE_PLACEHOLDER!r} placeholder."
+ )
+ injection = f""
+ return template.replace(TIMELINE_PLACEHOLDER, injection, 1)
+
+
+def _read_jsonl(path: Path) -> list[dict[str, Any]]:
+ """Read newline-delimited JSON. Skip blank lines, raise on parse errors."""
+ events: list[dict[str, Any]] = []
+ for lineno, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
+ line = raw.strip()
+ if not line:
+ continue
+ try:
+ events.append(json.loads(line))
+ except json.JSONDecodeError as exc:
+ raise typer.BadParameter(f"{path}: line {lineno} is not valid JSON: {exc}") from exc
+ return events
+
+
+def _is_replayable(event: dict[str, Any]) -> bool:
+ """Drop retry / failure events — replay ignores transient retries."""
+ if event.get("retry_of_event_id"):
+ return False
+ if event.get("decision") == "retry":
+ return False
+ # response_code != 0 implies a failed retrieval — also skip.
+ return "response_code" not in event or event.get("response_code") in (None, 0)
+
+
+def _validate_schema(events: list[dict[str, Any]], source: str) -> list[str]:
+ """Return a list of warning strings for events with non-"1" schema_version."""
+ warnings: list[str] = []
+ for event in events:
+ if event.get("schema_version") != SCHEMA_VERSION:
+ warnings.append(
+ f"{source}: event {event.get('event_id', '')} "
+ f"has schema_version={event.get('schema_version')!r} "
+ f"(expected {SCHEMA_VERSION!r})"
+ )
+ return warnings
+
+
+def merge_events(
+ retrieval_events: list[dict[str, Any]], growth_events: list[dict[str, Any]]
+) -> list[dict[str, Any]]:
+ """Merge two streams into one timeline.
+
+ Sort key is ``(timestamp_utc, actor_id, seq)``. ISO-8601 timestamps with a
+ fixed millisecond format and trailing ``Z`` sort lexicographically. The
+ sort is stable — events with identical keys retain their input order, so
+ we tag each with ``event_kind`` (``retrieval`` / ``growth``) before
+ merging to disambiguate downstream.
+ """
+ tagged: list[dict[str, Any]] = []
+ for event in retrieval_events:
+ # Note: we mutate via a shallow copy so the caller's list stays intact.
+ e = dict(event)
+ e.setdefault("event_kind", "retrieval")
+ tagged.append(e)
+ for event in growth_events:
+ e = dict(event)
+ e.setdefault("event_kind", "growth")
+ tagged.append(e)
+
+ tagged.sort(
+ key=lambda e: (
+ e.get("timestamp_utc", ""),
+ e.get("actor_id", ""),
+ e.get("seq", 0),
+ )
+ )
+ return tagged
+
+
+def _try_load_ir_artifacts(
+ pkg_dir: Path,
+) -> tuple[dict[str, Any] | None, dict[str, Any] | None, list[str]]:
+ """Best-effort load of compiled IR + DOT layout for a package.
+
+ Returns ``(ir, layout, warnings)``. Either of ``ir``/``layout`` may
+ be ``None`` when:
+
+ * the package has no ``.gaia/ir.json`` (e.g. the unit-test fixture),
+ * graphviz ``dot`` isn't on ``PATH``,
+ * compilation fails for any reason.
+
+ Replay still renders in those cases — without round beliefs / pinned
+ layout, but with the timeline + tick + structured-detail features
+ intact.
+ """
+ warnings: list[str] = []
+
+ # Try compiling the package fresh — that gives us the canonical IR
+ # used by `gaia inspect starmap` / `gaia run infer`. If compilation fails (no
+ # pyproject.toml, missing src/, etc.), fall back to the on-disk
+ # ``.gaia/ir.json`` if present.
+ ir: dict[str, Any] | None = None
+ try:
+ ensure_package_env(pkg_dir)
+ loaded = load_gaia_package(str(pkg_dir))
+ apply_package_priors(loaded)
+ compiled = compile_loaded_package_artifact(loaded)
+ ir = compiled.to_json()
+ except (GaiaPackagingError, Exception) as exc:
+ warnings.append(f"compilation skipped: {exc}")
+ ir_json_path = pkg_dir / ".gaia" / "ir.json"
+ if ir_json_path.is_file():
+ try:
+ ir = json.loads(ir_json_path.read_text(encoding="utf-8"))
+ warnings.append(f"using stored IR at {ir_json_path}")
+ except json.JSONDecodeError as parse_err:
+ warnings.append(f"stored IR is invalid JSON: {parse_err}")
+ ir = None
+ else:
+ ir = None
+
+ # Pinned layout requires a working IR + graphviz. Skip silently on
+ # failure (warnings surface to the CLI caller).
+ layout: dict[str, Any] | None = None
+ if ir is not None:
+ try:
+ param_data = param_data_from_ir_metadata(ir)
+ exported_ids = {k["id"] for k in ir.get("knowledges", []) if k.get("exported")}
+ graph_json = generate_graph_json(
+ ir,
+ beliefs_data=None,
+ param_data=param_data,
+ exported_ids=exported_ids,
+ )
+ dot_source = to_dot(graph_json)
+ layout = compute_dot_layout(dot_source)
+ # Re-key knowledge layout entries to raw lkm_ids so the
+ # frontend (which admits nodes by event-side id) can find
+ # their pinned coordinates. See rekey_layout_to_lkm_ids docs.
+ layout, rekey_warns = rekey_layout_to_lkm_ids(layout, ir)
+ warnings.extend(rekey_warns)
+ # Decorate every layout entry with kind + styling info pulled
+ # from the IR so the replay frontend can render strategies as
+ # ellipses and operators as hexagons (red for contradictions)
+ # at their pinned positions on first admission. Without this
+ # the canvas can't tell strat_ from oper_ from a normal
+ # claim and silently degrades to claim-only rendering.
+ annotate_layout_with_kinds(layout, ir)
+ except FileNotFoundError as exc:
+ warnings.append(f"pinned layout skipped: {exc}")
+ except Exception as exc:
+ warnings.append(f"pinned layout failed: {exc}")
+
+ return ir, layout, warnings
+
+
+def build_timeline_payload(
+ retrieval_events: list[dict[str, Any]],
+ growth_events: list[dict[str, Any]],
+ *,
+ package_name: str | None = None,
+ pkg_dir: Path | None = None,
+) -> dict[str, Any]:
+ """Construct the JSON payload the frontend reads from ``window.TIMELINE_DATA``.
+
+ Pulled out as a function so unit tests can call it on synthetic
+ inputs without touching the filesystem.
+ """
+ replayable_retrievals = [e for e in retrieval_events if _is_replayable(e)]
+ replayable_growths = [e for e in growth_events if _is_replayable(e)]
+ merged = merge_events(replayable_retrievals, replayable_growths)
+ ticks = split_into_ir_ticks(merged)
+
+ final_layout: dict[str, Any] | None = None
+ round_beliefs: dict[str, dict[str, float]] = {}
+ rounds_in_order: list[str] = collect_round_order(merged)
+ build_warnings: list[str] = []
+
+ ir_for_survival: dict[str, Any] | None = None
+ layout_for_survival: dict[str, Any] | None = None
+ if pkg_dir is not None:
+ ir, layout, warns = _try_load_ir_artifacts(pkg_dir)
+ build_warnings.extend(warns)
+ if layout is not None and ir is not None:
+ # Bridge event-side strategy / operator symbols (gfac_*,
+ # human-readable contradiction ids) to their strat_ /
+ # oper_ pinned positions so they don't pile up at the
+ # canvas centre.
+ layout, bridge_warns = bridge_event_symbols_to_layout(layout, ir, merged)
+ build_warnings.extend(bridge_warns)
+ if layout is not None:
+ final_layout = layout
+ layout_for_survival = layout
+ if ir is not None:
+ round_beliefs = compute_round_beliefs(ir, merged)
+ ir_for_survival = ir
+
+ # Mark each IR-tick with whether its action survives into the final
+ # compiled IR. Orphan ticks (action symbols that the agent admitted
+ # mid-run but later merged/repaired away) get `survives_to_final=False`
+ # so the frontend skips them on the canvas — keeping the hard
+ # invariant that the replay's final state equals the static SVG.
+ ticks, survival_warnings = annotate_ticks_with_survival(
+ ticks, merged, layout_for_survival, ir_for_survival
+ )
+ build_warnings.extend(survival_warnings)
+
+ # Topologically reorder surviving ticks so a strategy / operator
+ # tick fires only after all its referenced claims are admitted. This
+ # turns the IR-tick axis from a chronological-event axis into a
+ # logical-dependency axis: the lkm-to-gaia agent occasionally admits
+ # a contradiction operator before all of its variable claims are on
+ # canvas (later revising which claims it references). Replayed in
+ # chronological order, that produces transient frames where a
+ # hexagon's edges fan into not-yet-drawn nodes. The reorder uses
+ # original tick_index as a tiebreaker so chronology is preserved
+ # whenever no dependency forces a swap.
+ ticks, topo_warnings = topo_reorder_ticks(ticks, merged, layout_for_survival, ir_for_survival)
+ build_warnings.extend(topo_warnings)
+
+ return {
+ "schema_version": SCHEMA_VERSION,
+ "package_name": package_name,
+ "retrieval_count": len(replayable_retrievals),
+ "growth_count": len(replayable_growths),
+ "events": merged,
+ "ticks": ticks,
+ "rounds": rounds_in_order,
+ "round_beliefs": round_beliefs,
+ "final_layout": final_layout,
+ "build_warnings": build_warnings,
+ }
+
+
+def starmap_replay_command(
+ path: str = typer.Argument(".", help="Path to knowledge package directory"),
+ out: str = typer.Option(
+ None,
+ "--out",
+ help=(
+ "Output file. Defaults to '.gaia/starmap-replay.html' relative to "
+ "the package directory; absolute paths are honored as-is."
+ ),
+ ),
+) -> None:
+ """Emit an HTML replay of a package's lkm-discovery run.
+
+ Reads ``/artifacts/lkm-discovery/retrieval_log.jsonl`` and
+ ``/artifacts/lkm-discovery/graph_growth_log.jsonl`` (the two
+ JSONL logs an ``lkm-to-gaia`` orchestrator + worker pair leave behind),
+ merges them on ``(timestamp_utc, actor_id, seq)``, drops retry /
+ failure events, splits each event into per-``gaia_action`` IR-ticks,
+ and writes a single self-contained HTML page that plays back the run
+ on a pinned canonical layout. Round-by-round beliefs are computed by
+ re-running BP on the compiled IR truncated to each round's
+ cumulative knowledge set.
+
+ Examples:
+ # Default — write .gaia/starmap-replay.html into the package:
+ gaia inspect starmap-replay path/to/pkg
+
+ # Custom output path:
+ gaia inspect starmap-replay path/to/pkg --out figures/replay.html
+ """
+ pkg_dir = Path(path).resolve()
+ if not pkg_dir.is_dir():
+ typer.echo(f"Error: {pkg_dir} is not a directory.", err=True)
+ raise typer.Exit(1)
+
+ artifacts_dir = pkg_dir / ARTIFACTS_SUBDIR
+ retrieval_log = artifacts_dir / RETRIEVAL_LOG_NAME
+ growth_log = artifacts_dir / GROWTH_LOG_NAME
+
+ missing = [p for p in (retrieval_log, growth_log) if not p.is_file()]
+ if missing:
+ for p in missing:
+ typer.echo(f"Error: missing timeline log: {p}", err=True)
+ typer.echo(
+ "Run the lkm-to-gaia discovery pipeline first; both logs must "
+ f"exist under {artifacts_dir}.",
+ err=True,
+ )
+ raise typer.Exit(1)
+
+ retrieval_events = _read_jsonl(retrieval_log)
+ growth_events = _read_jsonl(growth_log)
+
+ for warning in _validate_schema(retrieval_events, str(retrieval_log)):
+ typer.echo(f"Warning: {warning}")
+ for warning in _validate_schema(growth_events, str(growth_log)):
+ typer.echo(f"Warning: {warning}")
+
+ payload = build_timeline_payload(
+ retrieval_events,
+ growth_events,
+ package_name=pkg_dir.name,
+ pkg_dir=pkg_dir,
+ )
+ for warning in payload.get("build_warnings", []):
+ typer.echo(f"Note: {warning}")
+
+ timeline_json = json.dumps(payload, ensure_ascii=False)
+
+ try:
+ template = _load_template()
+ content = _render_html(template, timeline_json)
+ except RuntimeError as exc:
+ typer.echo(str(exc), err=True)
+ raise typer.Exit(1) from exc
+ except FileNotFoundError as exc:
+ typer.echo(
+ "Error: starmap-replay template asset not found. The viz/ bundle "
+ f"may not have been shipped: {exc}",
+ err=True,
+ )
+ raise typer.Exit(1) from exc
+
+ out_path = Path(out) if out is not None else Path(DEFAULT_OUT_RELATIVE)
+ if not out_path.is_absolute():
+ out_path = pkg_dir / out_path
+ out_path.parent.mkdir(parents=True, exist_ok=True)
+ out_path.write_text(content, encoding="utf-8")
+
+ tick_count = len(payload["ticks"])
+ rounds_count = len(payload["rounds"])
+ typer.echo(
+ f"Wrote starmap replay to {out_path} "
+ f"({payload['retrieval_count']} retrievals, "
+ f"{payload['growth_count']} growth events, "
+ f"{len(payload['events'])} total, "
+ f"{tick_count} IR-ticks, "
+ f"{rounds_count} rounds)"
+ )
diff --git a/gaia/cli/commands/trace.py b/gaia/cli/commands/trace.py
index 5f366dd62..c3e83e3dc 100644
--- a/gaia/cli/commands/trace.py
+++ b/gaia/cli/commands/trace.py
@@ -1,4 +1,4 @@
-"""gaia trace — public CLI sub-app(与 gaia inquiry 平行)。
+"""Public `gaia trace` CLI sub-app.
Commands per ARM Trace v1:
verify — 仅 schema + hash chain 校验,秒级 fail-fast
@@ -13,14 +13,13 @@
from __future__ import annotations
-from typing import Optional
-
import typer
-from gaia.trace.hashing import compute_events_root, compute_manifest_hash, recompute_chain
-from gaia.trace.loader import load_trace
-from gaia.trace.render import render_json, render_markdown, render_text
-from gaia.trace.review import run_trace_review
+from gaia.engine.trace.hashing import compute_events_root, compute_manifest_hash, recompute_chain
+from gaia.engine.trace.loader import load_trace
+from gaia.engine.trace.render import render_json, render_markdown, render_text
+from gaia.engine.trace.review import run_trace_review
+from gaia.engine.trace.schema import Trace
trace_app = typer.Typer(
name="trace",
@@ -39,7 +38,7 @@ def verify_command(
trace_path: str = typer.Argument(..., help="Path to trace file (.json/.jsonl)."),
quiet: bool = typer.Option(False, "--quiet", help="Suppress non-error output."),
) -> None:
- """schema + hash chain 校验。
+ """Verify trace schema and hash chain.
exit 0:clean
exit 1:hash chain / manifest mismatch
@@ -56,58 +55,76 @@ def verify_command(
trace = res.trace
assert trace is not None # 没有 issues 就一定有 trace
- chain = recompute_chain(trace.events)
+ errors = _trace_verify_errors(trace, recompute_chain(trace.events))
+ if errors:
+ _raise_trace_verify_failure(errors, quiet=quiet)
+ _emit_trace_verify_ok(trace, quiet=quiet)
+
+
+# ---------------------------------------------------------------------------
+# review
+# ---------------------------------------------------------------------------
+
+
+_SUPPORTED_REVIEW_MODES = {"trace", "publish"}
+
+
+def _trace_chain_mismatches(trace: Trace, chain: list[str]) -> list[str]:
+ """Return the first event prev-hash mismatch after genesis."""
+ for i in range(1, len(trace.events)):
+ if trace.events[i].prev_hash != chain[i - 1]:
+ return [f"events[{i}] (seq={trace.events[i].seq}) prev_hash mismatch"]
+ return []
+
+
+def _trace_verify_errors(trace: Trace, chain: list[str]) -> list[str]:
+ """Return hash-chain and manifest mismatches for a loaded trace."""
expected_root = compute_events_root(trace.events)
expected_manifest_hash = compute_manifest_hash(trace.manifest)
-
errors: list[str] = []
- # 链
+
if trace.events:
- from gaia.trace.hashing import GENESIS_PREV_HASH
+ from gaia.engine.trace.hashing import GENESIS_PREV_HASH
if trace.events[0].prev_hash != GENESIS_PREV_HASH:
errors.append(f"events[0].prev_hash != GENESIS ({trace.events[0].prev_hash!r})")
- for i in range(1, len(trace.events)):
- if trace.events[i].prev_hash != chain[i - 1]:
- errors.append(f"events[{i}] (seq={trace.events[i].seq}) prev_hash mismatch")
- break
+ errors.extend(_trace_chain_mismatches(trace, chain))
if trace.manifest.events_root != expected_root:
errors.append("manifest.events_root mismatch")
if trace.manifest.manifest_hash and trace.manifest.manifest_hash != expected_manifest_hash:
errors.append("manifest.manifest_hash mismatch")
-
- if errors:
- if not quiet:
- typer.echo("[verify] FAIL", err=True)
- for e in errors:
- typer.echo(f" - {e}", err=True)
- raise typer.Exit(1)
-
- if not quiet:
- typer.echo("[verify] OK")
- typer.echo(f" events : {len(trace.events)}")
- typer.echo(f" events_root : {expected_root}")
- typer.echo(f" manifest_hash : {trace.manifest.manifest_hash or '(none)'}")
+ return errors
-# ---------------------------------------------------------------------------
-# review
-# ---------------------------------------------------------------------------
+def _emit_trace_verify_ok(trace: Trace, *, quiet: bool) -> None:
+ """Print successful trace verification details."""
+ if quiet:
+ return
+ typer.echo("[verify] OK")
+ typer.echo(f" events : {len(trace.events)}")
+ typer.echo(f" events_root : {compute_events_root(trace.events)}")
+ typer.echo(f" manifest_hash : {trace.manifest.manifest_hash or '(none)'}")
-_SUPPORTED_REVIEW_MODES = {"trace", "publish"}
+def _raise_trace_verify_failure(errors: list[str], *, quiet: bool) -> None:
+ """Print trace verification errors and exit with status 1."""
+ if not quiet:
+ typer.echo("[verify] FAIL", err=True)
+ for error in errors:
+ typer.echo(f" - {error}", err=True)
+ raise typer.Exit(1)
@trace_app.command("review")
def review_command(
trace_path: str = typer.Argument(..., help="Path to trace file (.json/.jsonl)."),
mode: str = typer.Option("trace", "--mode", help="Ranking mode: trace|publish."),
- package: Optional[str] = typer.Option(
+ package: str | None = typer.Option(
None, "--package", help="Gaia package path used to resolve claim_ref review_ids."
),
json_out: bool = typer.Option(False, "--json", help="Emit JSON report (deterministic)."),
markdown_out: bool = typer.Option(False, "--markdown", help="Emit Markdown report."),
- snapshot_dir: Optional[str] = typer.Option(
+ snapshot_dir: str | None = typer.Option(
None, "--snapshot-dir", help="Override snapshot output directory."
),
strict: bool = typer.Option(
@@ -116,7 +133,7 @@ def review_command(
help="Exit non-zero whenever any error/warning diagnostic is present.",
),
) -> None:
- """完整八段 review。
+ """Run the full ARM trace review.
exit 0:clean
exit 1:含 error 级 diagnostic 或 --strict 下含 warning
@@ -163,12 +180,12 @@ def review_command(
def show_command(
trace_path: str = typer.Argument(..., help="Path to trace file (.json/.jsonl)."),
limit: int = typer.Option(50, "--limit", help="Max events to print (0 = all)."),
- kind: Optional[str] = typer.Option(
+ kind: str | None = typer.Option(
None, "--kind", help="Filter by event kind (decision/tool_call/...)."
),
json_out: bool = typer.Option(False, "--json", help="Emit JSONL of selected events."),
) -> None:
- """打印事件流(tactic_log 风格)。
+ """Print the trace event stream.
schema_violation 时仍然尽量打可解析事件 + 报错到 stderr。
"""
diff --git a/gaia/cli/main.py b/gaia/cli/main.py
index 943b74de8..655cbabca 100644
--- a/gaia/cli/main.py
+++ b/gaia/cli/main.py
@@ -1,15 +1,31 @@
-"""Gaia CLI — knowledge package authoring toolkit."""
+"""Gaia CLI — knowledge package authoring toolkit.
+
+The CLI organizes verbs into 6 groups + `trace` independent:
+
+ build init / compile / check
+ run infer / render
+ inspect starmap / starmap-replay
+ review (empty skeleton — held for downstream reviewer tooling)
+ inquiry (sub-app: focus / review / obligation / hypothesis / tactics / reject)
+ pkg add / register
+ trace (sub-app, NOT part of the 6 groups: verify / review / show)
+
+See `docs/migration.md` for guidance on moving off pre-alpha-0 invocations.
+"""
import typer
+from gaia._meta import IR_SCHEMA, get_channel, get_commit, get_library_version
from gaia.cli.commands.add import add_command
from gaia.cli.commands.check import check_command
from gaia.cli.commands.compile import compile_command
from gaia.cli.commands.infer import infer_command
from gaia.cli.commands.init import init_command
+from gaia.cli.commands.inquiry import inquiry_app
from gaia.cli.commands.register import register_command
from gaia.cli.commands.render import render_command
-from gaia.cli.commands.inquiry import inquiry_app
+from gaia.cli.commands.starmap import starmap_command
+from gaia.cli.commands.starmap_replay import starmap_replay_command
from gaia.cli.commands.trace import trace_app
app = typer.Typer(
@@ -19,19 +35,127 @@
)
+def _version_callback(value: bool) -> None:
+ if value:
+ typer.echo(f"gaia-lang {get_library_version()}")
+ typer.echo(f"channel: {get_channel()}")
+ typer.echo(f"commit: {get_commit()}")
+ typer.echo(f"ir_schema: {IR_SCHEMA}")
+ raise typer.Exit()
+
+
@app.callback()
-def _callback() -> None:
+def _callback(
+ version: bool = typer.Option(
+ False,
+ "--version",
+ callback=_version_callback,
+ is_eager=True,
+ help="Show version, channel, commit, and ir_schema; then exit.",
+ ),
+) -> None:
"""Gaia — knowledge package authoring toolkit."""
-app.command(name="add")(add_command)
-app.command(name="compile")(compile_command)
-app.command(name="check")(check_command)
-app.command(name="infer")(infer_command)
-app.command(name="init")(init_command)
-app.command(name="register")(register_command)
-app.command(name="render")(render_command)
+# --------------------------------------------------------------------------- #
+# build — init / compile / check #
+# --------------------------------------------------------------------------- #
+
+build_app = typer.Typer(
+ name="build",
+ help="Build artifacts (init / compile / check).",
+ no_args_is_help=True,
+)
+build_app.command(name="init")(init_command)
+build_app.command(name="compile")(compile_command)
+build_app.command(name="check")(check_command)
+app.add_typer(build_app, name="build")
+
+
+# --------------------------------------------------------------------------- #
+# run — infer / render #
+# --------------------------------------------------------------------------- #
+
+run_app = typer.Typer(
+ name="run",
+ help="Run inference and rendering (infer / render).",
+ no_args_is_help=True,
+)
+run_app.command(name="infer")(infer_command)
+run_app.command(name="render")(render_command)
+app.add_typer(run_app, name="run")
+
+
+# --------------------------------------------------------------------------- #
+# inspect — starmap / starmap-replay #
+# --------------------------------------------------------------------------- #
+
+inspect_app = typer.Typer(
+ name="inspect",
+ help="Inspect compiled artifacts (starmap / starmap-replay).",
+ no_args_is_help=True,
+)
+inspect_app.command(name="starmap")(starmap_command)
+inspect_app.command(name="starmap-replay")(starmap_replay_command)
+app.add_typer(inspect_app, name="inspect")
+
+
+# --------------------------------------------------------------------------- #
+# review — reviewer tooling skeleton (alpha 0: empty) #
+# --------------------------------------------------------------------------- #
+#
+# Per 协作单 二·共识, the `review` top-level group lands as a help-visible
+# empty skeleton so downstream reviewer-tooling work has a stable home.
+# It is *different* from `gaia inquiry review` and `gaia trace review` —
+# those are pre-existing inner subcommands, untouched by alpha 0.
+
+review_app = typer.Typer(
+ name="review",
+ help="Reviewer tooling (alpha 0: skeleton only — no commands yet).",
+ no_args_is_help=True,
+)
+
+
+@review_app.callback(invoke_without_command=True)
+def _review_skeleton(ctx: typer.Context) -> None:
+ """Placeholder for the reviewer-tooling group.
+
+ Alpha 0 ships this group as a help-visible skeleton; concrete commands
+ will arrive in a later release. Invoking `gaia review` directly with no
+ subcommand prints the help text (no_args_is_help=True).
+ """
+ if ctx.invoked_subcommand is None:
+ # no_args_is_help handles the bare case; this branch is defensive.
+ return
+
+
+app.add_typer(review_app, name="review")
+
+
+# --------------------------------------------------------------------------- #
+# inquiry — existing sub-app (internals untouched) #
+# --------------------------------------------------------------------------- #
app.add_typer(inquiry_app, name="inquiry")
app.add_typer(inquiry_app, name="inquery", hidden=True) # typo alias
+
+
+# --------------------------------------------------------------------------- #
+# pkg — add / register #
+# --------------------------------------------------------------------------- #
+
+pkg_app = typer.Typer(
+ name="pkg",
+ help="Package operations (add / register).",
+ no_args_is_help=True,
+)
+pkg_app.command(name="add")(add_command)
+pkg_app.command(name="register")(register_command)
+app.add_typer(pkg_app, name="pkg")
+
+
+# --------------------------------------------------------------------------- #
+# trace — existing sub-app, independent of the 6 groups #
+# --------------------------------------------------------------------------- #
+
app.add_typer(trace_app, name="trace")
diff --git a/gaia/cli/starmap_assets/__init__.py b/gaia/cli/starmap_assets/__init__.py
new file mode 100644
index 000000000..797b3f04d
--- /dev/null
+++ b/gaia/cli/starmap_assets/__init__.py
@@ -0,0 +1,8 @@
+"""Static assets bundled with `gaia inspect starmap`.
+
+Holds the single-file HTML template (`template.html`) into which the
+CLI injects a JSON graph payload. The current template is a minimal
+placeholder; a richer interactive bundle replaces it later without
+any change to the CLI plumbing as long as the
+```` placeholder is preserved in ````.
+"""
diff --git a/gaia/cli/starmap_assets/template.html b/gaia/cli/starmap_assets/template.html
new file mode 100644
index 000000000..03bd4fb62
--- /dev/null
+++ b/gaia/cli/starmap_assets/template.html
@@ -0,0 +1,364 @@
+
+
+
+
+
+ Gaia Starmap
+
+
+
+
+
+
+
+
+
gaia · starmap
+
+
+
+
+
+
+
belief 0
+
belief 0.5 / unknown
+
belief 1
+
strategy
+
operator
+
+
loading…
+
+
+
diff --git a/gaia/cli/starmap_replay_assets/__init__.py b/gaia/cli/starmap_replay_assets/__init__.py
new file mode 100644
index 000000000..b8af8601b
--- /dev/null
+++ b/gaia/cli/starmap_replay_assets/__init__.py
@@ -0,0 +1,9 @@
+"""Static assets bundled with `gaia inspect starmap-replay`.
+
+Holds the single-file HTML template (``template.html``) into which the
+CLI injects the JSONL timeline payload. Mirrors the
+``gaia.cli.starmap_assets`` shipping pattern: ``viz/`` builds a
+self-contained bundle, the ship script copies it here, and the CLI
+substitutes the ```` placeholder in ````
+with a ```` tag at run time.
+"""
diff --git a/gaia/cli/starmap_replay_assets/template.html b/gaia/cli/starmap_replay_assets/template.html
new file mode 100644
index 000000000..7f31ef625
--- /dev/null
+++ b/gaia/cli/starmap_replay_assets/template.html
@@ -0,0 +1,79 @@
+
+
+
+
+
+ Gaia Starmap — Replay
+
+
+
+
+
+
+
+
gaia · starmap · replay
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
scroll to zoom · drag canvas to pan · hover for prior & belief · double-click to reset