Skip to content
Closed
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to this Hermes profile distribution are documented here.

## 0.7.0

- Added an interactive profile design wizard with class-based archetypes and companion bundles.
- Added `templates/wizard/classes.yaml` for configurable profile classes.
- Added `templates/wizard/bundles.yaml` for balanced extension presets.
- Added `scripts/profile_wizard.py` for guided profile params generation.
- Added README path for wizard-driven profile authoring.

## 0.6.3

- Added real Hermes LLM calls to the local web demo pipeline: one call expands the sentence into a mature profile prompt, another reviews generated profile quality.
Expand Down Expand Up @@ -70,3 +78,4 @@ All notable changes to this Hermes profile distribution are documented here.
## 0.1.0

- Initial Hermes profile template with deterministic generation, validation, bundled profile-craft skill, catalog snippets, and installable distribution metadata.

28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,33 @@ hermes profile install . --name security-reviewer-local --yes

This path is deterministic and quick, but less expressive than the installed `profile-architect` prompt workflow.

### Path 5: Deterministic generation from a params file
### Path 5: Interactive profile design wizard

Use this when you want guided class selection instead of writing prompt prose from scratch. Implements issue #13.

```bash
python3 scripts/profile_wizard.py
```

The wizard reads class and bundle definitions from `templates/wizard/classes.yaml` and `templates/wizard/bundles.yaml`, lets you pick an archetype, writes `profile.params.yaml`, and exits. For non-interactive use:

```bash
python3 scripts/profile_wizard.py --non-interactive
```

To write to a custom path or overwrite an existing params file:

```bash
python3 scripts/profile_wizard.py --output my-profile.yaml --force
```

You can then generate from the params file with:

```bash
python3 scripts/generate_profile.py --params profile.params.yaml --output <target-dir>
```

### Path 6: Deterministic generation from a params file

Use this when you want reproducible profile generation, code review, or repeatable builds.

Expand Down
2 changes: 1 addition & 1 deletion distribution.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: hermes-profile-template
version: 0.6.3
version: 0.7.0
description: "Prompt-to-repo authoring system with one-sentence generation, demo assets, diagrams, and installable Hermes Agent profile distributions."
hermes_requires: ">=0.12.0"
author: "CodeGraphTheory"
Expand Down
199 changes: 199 additions & 0 deletions scripts/profile_wizard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""Interactive Hermes profile design wizard.

Prompts for an archetype class and optional bundle, then writes profile.params.yaml.
"""
from __future__ import annotations

import argparse
import sys
from pathlib import Path

import yaml # type: ignore

TEMPLATE_DIR = Path(__file__).resolve().parents[1] / "templates" / "wizard"
DEFAULT_CLASSES = TEMPLATE_DIR / "classes.yaml"
DEFAULT_BUNDLES = TEMPLATE_DIR / "bundles.yaml"
DEFAULT_OUTPUT = Path("profile.params.yaml")


def load_yaml(path: Path) -> dict:
if not path.exists():
raise FileNotFoundError(f"Wizard config not found: {path}")
data = yaml.safe_load(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ValueError(f"Wizard config must be a YAML mapping: {path}")
return data


def list_choices(items: list[dict], key: str = "slug") -> list[str]:
return [item.get(key, item.get("name", "")) for item in items if isinstance(item, dict)]


def pick(prompt: str, choices: list[str], default: str | None = None) -> str:
print(prompt)
for idx, choice in enumerate(choices, 1):
print(f" {idx}) {choice}")
default_idx = 1
if default and default in choices:
default_idx = choices.index(default) + 1
while True:
raw = input(f"Select [{default_idx}]: ").strip() or str(default_idx)
if raw.isdigit():
idx = int(raw) - 1
if 0 <= idx < len(choices):
return choices[idx]
print(f"Please enter 1-{len(choices)}")


def ask_optional(prompt: str, default: bool = False) -> bool:
suffix = "[Y/n]" if default else "[y/N]"
raw = input(f"{prompt} {suffix}: ").strip().lower() or ("y" if default else "n")
return raw.startswith("y")


def build_params(archetype: dict, bundle: dict | None) -> dict:
params: dict = {
"name": archetype.get("default_name", "custom-profile"),
"display_name": archetype.get("default_display_name", "Custom Hermes Profile"),
"description": archetype.get("description", ""),
"author": archetype.get("author", "profile author"),
"version": "0.1.0",
"license": "MIT",
"hermes_requires": ">=0.12.0",
"model_provider": archetype.get("model_provider", "openrouter"),
"model_default": archetype.get("model_default", "anthropic/claude-sonnet-4"),
"template_source": {
"name": "codegraphtheory/hermes-profile-template",
"url": "https://github.com/codegraphtheory/hermes-profile-template",
"relationship": "generated-from-template",
},
"toolsets": list(archetype.get("toolsets", [])),
"env_requires": list(archetype.get("env_requires", [])),
"principles": list(archetype.get("principles", [])),
"scope": list(archetype.get("scope", [])),
"refusals": list(archetype.get("refusals", [])),
"output_contract": list(archetype.get("output_contract", [])),
"github_topics": list(archetype.get("github_topics", [])),
}

if bundle:
if bundle.get("extends_toolsets"):
existing = params["toolsets"]
for item in bundle["extends_toolsets"]:
if item not in existing:
existing.append(item)
if bundle.get("extends_principles"):
params["principles"].extend(
[item for item in bundle["extends_principles"] if item not in params["principles"]]
)
if bundle.get("extends_scope"):
params["scope"].extend(
[item for item in bundle["extends_scope"] if item not in params["scope"]]
)
if bundle.get("default_name"):
params["name"] = bundle["default_name"]
if bundle.get("default_display_name"):
params["display_name"] = bundle["default_display_name"]
if bundle.get("description"):
params["description"] = bundle["description"]
return params


def write_params(path: Path, params: dict, *, force: bool = False) -> None:
if path.exists() and not force:
raise SystemExit(f"Refusing to overwrite {path}. Re-run with --force or choose --output.")
path.parent.mkdir(parents=True, exist_ok=True)
yaml.safe_dump(
params,
path.open("w"),
sort_keys=False,
allow_unicode=True,
default_flow_style=False,
)


def try_write_params(path: Path, params: dict, *, force: bool = False) -> None:
try:
write_params(path, params, force=force)
print(f"\nWrote {path}")
except SystemExit:
raise
except Exception as exc: # noqa: BLE001
raise SystemExit(f"Failed to write {path}: {exc}")


def run_wizard(classes_path: Path, bundles_path: Path, output_path: Path, *, force: bool = False) -> int:
if not classes_path.exists():
raise SystemExit(f"Missing classes config: {classes_path}")
classes = load_yaml(classes_path).get("classes", [])
if not classes:
raise SystemExit(f"No classes defined in {classes_path}")

class_names = list_choices(classes, "slug")
print("Select a profile class.\n")
chosen_class_slug = pick("Profile class:", class_names)
archetype = next(item for item in classes if item.get("slug") == chosen_class_slug)

bundle = None
bundles = []
if bundles_path.exists():
bundles = load_yaml(bundles_path).get("bundles", [])

if bundles:
bundle_names = ["<none>"] + list_choices(bundles, "name")
bundle_slug = pick("\nOptional companion bundle:", bundle_names, default=bundle_names[0])
if bundle_slug != "<none>":
bundle = next(item for item in bundles if item.get("name") == bundle_slug)

confirm = ask_optional(f"\nWrite profile params to {output_path}?", default=True)
if not confirm:
print("Aborted.")
return 0

params = build_params(archetype, bundle)
try_write_params(output_path, params, force=force)
return 0


def main() -> int:
parser = argparse.ArgumentParser(description="Interactive profile design wizard")
parser.add_argument("--classes", default=str(DEFAULT_CLASSES))
parser.add_argument("--bundles", default=str(DEFAULT_BUNDLES))
parser.add_argument("--output", default=str(DEFAULT_OUTPUT))
parser.add_argument("--non-interactive", action="store_true", help="Skip prompts and use defaults")
parser.add_argument("--force", action="store_true", help="Overwrite output file if it exists")
args = parser.parse_args()

if args.non_interactive:
params = {
"name": "custom-profile",
"display_name": "Custom Hermes Profile",
"description": "Interactive wizard output",
"author": "Hermes profile author",
"version": "0.1.0",
"license": "MIT",
"hermes_requires": ">=0.12.0",
"model_provider": "openrouter",
"model_default": "anthropic/claude-sonnet-4",
"template_source": {
"name": "codegraphtheory/hermes-profile-template",
"url": "https://github.com/codegraphtheory/hermes-profile-template",
"relationship": "generated-from-template",
},
"toolsets": ["file", "terminal", "skills", "web", "session_search", "clarify"],
"env_requires": [],
"principles": [],
"scope": [],
"refusals": [],
"output_contract": [],
"github_topics": [],
}
try_write_params(Path(args.output), params, force=args.force)
return 0

return run_wizard(Path(args.classes), Path(args.bundles), Path(args.output), force=args.force)


if __name__ == "__main__":
raise SystemExit(main())
57 changes: 57 additions & 0 deletions templates/wizard/bundles.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
bundles:
- name: Clean Room
extends_toolsets: []
extends_principles:
- Do not write to the live system unless requested.
- Treat every generation as a reviewable patch.
extends_scope:
- Produce isolated output with side effects explained and minimized.
default_name: clean-room-engineer
default_display_name: Clean Room Engineer
description: Confined engineering mode with reduced side effects for testing and review.
- name: Open Forge
extends_toolsets:
- browser
extends_principles:
- Leave public-facing work ready for viewer scrutiny.
- Favor visible actions over silent background jobs.
extends_scope:
- Publish or preview functional outputs when possible.
default_name: open-forge-artisan
default_display_name: Open Forge Artisan
description: Public-facing creative mode for polished shipped demos with explicit preview paths.
- name: Bound Scanner
extends_toolsets:
- x_search
extends_principles:
- Focus on traceable public evidence.
- Minimize reliance on unverified data sources.
extends_scope:
- Surface high-signal public materials.
- Summarize trust and freshness alongside findings.
default_name: bound-scout
default_display_name: Bound Recon Scout
description: Scoped research mode emphasizing traceability and freshness for discovery work.
- name: Deep Audit
extends_toolsets:
- session_search
extends_principles:
- Keep every decision tied to observed failure traces.
- Prioritize falsifiable signals over opinion.
extends_scope:
- Reproduction with observed system state.
- Failure-mode-first reporting.
default_name: deep-audit-warden
default_display_name: Deep Audit Warden
description: Investigative hardening mode focused on system state reproduction and failure tracing.
- name: Advisory Mirror
extends_principles:
- Declare reviewer mode explicitly.
- Keep judgments attributable to sources.
- Avoid acting on behalf of the user unless asked.
extends_scope:
- Issue advisory outputs rather than executing changes directly.
- Flag unresolved assumptions and missing data.
default_name: advisory-oracle
default_display_name: Advisory Mirror Oracle
description: Non-executing research advisor that preserves attribution and assumption boundaries.
Loading