快速了解
它能做什么
面向首次使用的交互式引导,会检查环境配置并帮助你开始 Ouroboros 项目。
本站提供的是中文说明,不代表该项目或 Plugin 自身提供中文界面;语言支持请以上游文档为准。
选择前先看
该技能提供 `/ouroboros:welcome` 入门流程。它会检查 Claude Code 或 Codex 是否已完成配置;如未完成,可引导进行设置;随后询问你的目标,并带你进入项目访谈、教程或文档。它还会处理可选的 Codex 模型迁移,并记录欢迎引导完成状态。
适合谁
希望完成引导配置并明确下一步操作的新老 Ouroboros 用户。
常见任务
- 检查 Ouroboros 是否已为 Codex 或 Claude Code 准备就绪。
- 配置支持的运行环境,或在暂不配置时继续查看基础引导。
- 开始苏格拉底式项目访谈、试用教程或阅读命令文档。
- 在升级后重新运行入门引导。
权限与数据
该引导会检查本地配置文件,并可能更新 Ouroboros 偏好设置。
权限- 读取用户主目录中的 Ouroboros 配置和偏好文件。
- 检查本地 Python、uv、Codex 及已配置 MCP 启动器是否可用。
- 仅在用户选择设置后,才可能运行 Ouroboros 设置命令。
- 在 `~/.ouroboros/prefs.json` 中保存欢迎引导完成时间、版本及可选模型迁移标记。
- 仅在用户选择迁移后,才可能改写四个旧版 Codex 模型字段。
- 仅当用户选择且 GitHub CLI 已认证时,才可能通过 GitHub CLI 为 `Q00/ouroboros` 仓库加星。
- 仅可选的仓库加星操作需要 GitHub CLI 认证。
局限
- 需要 Python 3.12 或更高版本,或者 PATH 中可用的 uv。
- 配置就绪与否取决于本地 Ouroboros、Codex 或 Claude 及 MCP 配置。
- 提供的证据记录了流程,但未显示该流程已在此环境中执行。
DSHub 已核对
- 固定 GitHub 提交中的完整技能文档已被采集。
- 来源记录识别出 MIT 许可证。
DSHub 未核对
- DSHub 未执行运行环境设置、文件写入、模型迁移或可选 GitHub 操作。
固定版本安装
主要操作
这个独立 Skill没有 DSH Plugin 安装操作,请根据源码文档使用真实交付方式。
维护者原文
Skill 使用说明
name: welcome description: "First-touch experience for new Ouroboros users"
/ouroboros:welcome
Interactive onboarding for new Ouroboros users.
Usage
/ouroboros:welcome # First-time or update onboarding
/ouroboros:welcome --skip # Skip welcome, mark as shown
/ouroboros:welcome --force # Force re-run welcome even if shown
Instructions
When this skill is invoked, follow this flow:
Python Runtime (Required)
Before running any shell snippet below, define this resolver in the same shell.
It accepts only Python 3.12 or newer, prefers python3 and then python, and
uses uv as the final fallback. Call ouroboros_python directly and quote every
argument passed to it; the function preserves arguments and heredoc/stdin input.
Only the probe and child interpreter discard inherited CPython path-selection
overrides; the caller shell keeps its environment unchanged.
ouroboros_python() {
if command -v python3 >/dev/null 2>&1 &&
(unset PYTHONHOME PYTHONPATH PYTHONPLATLIBDIR PYTHONEXECUTABLE __PYVENV_LAUNCHER__; command python3 -c 'import sys; raise SystemExit(sys.version_info < (3, 12))') >/dev/null 2>&1
then
(unset PYTHONHOME PYTHONPATH PYTHONPLATLIBDIR PYTHONEXECUTABLE __PYVENV_LAUNCHER__; command python3 "$@")
return
fi
if command -v python >/dev/null 2>&1 &&
(unset PYTHONHOME PYTHONPATH PYTHONPLATLIBDIR PYTHONEXECUTABLE __PYVENV_LAUNCHER__; command python -c 'import sys; raise SystemExit(sys.version_info < (3, 12))') >/dev/null 2>&1
then
(unset PYTHONHOME PYTHONPATH PYTHONPLATLIBDIR PYTHONEXECUTABLE __PYVENV_LAUNCHER__; command python "$@")
return
fi
if command -v uv >/dev/null 2>&1; then
(unset PYTHONHOME PYTHONPATH PYTHONPLATLIBDIR PYTHONEXECUTABLE __PYVENV_LAUNCHER__; command uv run --no-project --quiet --python '>=3.12' python "$@")
return
fi
printf '%s\n' 'Ouroboros skills require Python >= 3.12 or uv on PATH.' >&2
return 127
}
<!-- ouroboros-python-resolver:end -->Pre-Check: Already Completed?
First, check ~/.ouroboros/prefs.json for welcomeCompleted. For upgrades from older releases, also treat legacy welcomeShown: true as completed so the welcome prompt does not reappear forever:
PREFFILE="$HOME/.ouroboros/prefs.json"
if [ -f "$PREFFILE" ]; then
WELCOME_COMPLETED=$(ouroboros_python - <<'PY'
import json, os
path = os.path.expanduser('~/.ouroboros/prefs.json')
try:
prefs = json.load(open(path, encoding='utf-8'))
except Exception:
prefs = {}
if not isinstance(prefs, dict):
prefs = {}
print(prefs.get('welcomeCompleted') or ('legacy-welcomeShown' if prefs.get('welcomeShown') else ''))
PY
)
WELCOME_VERSION=$(ouroboros_python - <<'PY'
import json, os
path = os.path.expanduser('~/.ouroboros/prefs.json')
try:
prefs = json.load(open(path, encoding='utf-8'))
except Exception:
prefs = {}
if not isinstance(prefs, dict):
prefs = {}
print(prefs.get('welcomeVersion') or '')
PY
)
if [ -n "$WELCOME_COMPLETED" ] && [ "$WELCOME_COMPLETED" != "null" ]; then
ALREADY_COMPLETED="true"
fi
fi
Before honoring that completion marker, determine whether setup is ready for the active runtime. A previously completed welcome must never hide the setup gate from a user who chose 나중에 or whose setup was later removed.
First accept a completed Claude Code setup:
if ouroboros_python - "$HOME/.ouroboros/config.yaml" <<'PY'
from __future__ import annotations
import sys
from pathlib import Path
try:
import yaml
except ModuleNotFoundError:
yaml = None
config_path = Path(sys.argv[1])
def yaml_mapping(source: str) -> dict[str, dict[str, str]]:
"""Read the top-level mapping scalars this readiness gate owns."""
if yaml is not None:
loaded = yaml.safe_load(source) or {}
return loaded if isinstance(loaded, dict) else {}
parsed: dict[str, dict[str, str]] = {}
section: str | None = None
def scalar_value(raw: str) -> str:
return raw.strip().split(" #", 1)[0].strip().rstrip(",}").strip().strip("'\"")
def flow_mapping(raw: str) -> dict[str, str]:
value = raw.strip().split(" #", 1)[0].strip()
if not (value.startswith("{") and value.endswith("}")):
return {}
fields: dict[str, str] = {}
for part in value[1:-1].split(","):
key, separator, field_value = part.partition(":")
if separator:
fields[key.strip().strip("'\"")] = scalar_value(field_value)
return fields
for raw_line in source.splitlines():
if not raw_line.strip() or raw_line.lstrip().startswith("#"):
continue
indent = len(raw_line) - len(raw_line.lstrip())
key, separator, raw_value = raw_line.strip().partition(":")
if not separator:
continue
if indent == 0:
section = key.strip("'\"")
parsed[section] = flow_mapping(raw_value)
elif section is not None:
parsed.setdefault(section, {})[key.strip("'\"")] = scalar_value(raw_value)
return parsed
try:
config = yaml_mapping(config_path.read_text(encoding="utf-8"))
except (OSError, ValueError):
raise SystemExit(1)
orchestrator = config.get("orchestrator") if isinstance(config, dict) else None
llm = config.get("llm") if isinstance(config, dict) else None
# Existing YAML form: runtime_backend: claude. Parsing avoids assuming its order.
# The marketplace plugin owns its MCP capability. Host-owned
# ~/.claude/mcp.json is intentionally not part of SDK setup readiness.
ready = (
isinstance(orchestrator, dict)
and orchestrator.get("runtime_backend") in {"claude", "claude_mcp"}
and isinstance(llm, dict)
and llm.get("backend") == "claude"
)
raise SystemExit(0 if ready else 1)
PY
then
SETUP_READY="true"
fi
If SETUP_READY is not true, determine whether the Codex setup is ready:
CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}"
case "$CODEX_HOME_DIR" in
"~") CODEX_HOME_DIR="$HOME" ;;
"~/"*) CODEX_HOME_DIR="$HOME/${CODEX_HOME_DIR#"~/"}" ;;
esac
if ouroboros_python - "$HOME/.ouroboros/config.yaml" "$CODEX_HOME_DIR/config.toml" <<'PY'
from __future__ import annotations
import re
import os
import shutil
import sys
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # Python 3.10 and earlier hosts
tomllib = None
try:
import yaml
except ModuleNotFoundError:
yaml = None
config_path, codex_config_path = map(Path, sys.argv[1:])
def yaml_mapping(source: str) -> dict[str, dict[str, str]]:
"""Read only the top-level mapping scalars owned by this readiness gate."""
if yaml is not None:
loaded = yaml.safe_load(source) or {}
return loaded if isinstance(loaded, dict) else {}
parsed: dict[str, dict[str, str]] = {}
section: str | None = None
def scalar_value(raw: str) -> str:
return raw.strip().split(" #", 1)[0].strip().rstrip(",}").strip().strip("'\"")
def flow_mapping(raw: str) -> dict[str, str]:
value = raw.strip().split(" #", 1)[0].strip()
if not (value.startswith("{") and value.endswith("}")):
return {}
fields: dict[str, str] = {}
for part in value[1:-1].split(","):
key, separator, field_value = part.partition(":")
if separator:
fields[key.strip().strip("'\"")] = scalar_value(field_value)
return fields
for raw_line in source.splitlines():
if not raw_line.strip() or raw_line.lstrip().startswith("#"):
continue
indent = len(raw_line) - len(raw_line.lstrip())
key, separator, raw_value = raw_line.strip().partition(":")
if not separator:
continue
if indent == 0:
section = key.strip("'\"")
parsed[section] = flow_mapping(raw_value)
elif section is not None:
parsed[section][key.strip("'\"")] = scalar_value(raw_value)
return parsed
def toml_mcp_servers(source: str) -> dict[str, dict[str, object]]:
"""Read MCP server table membership when the host lacks ``tomllib``."""
servers: dict[str, dict[str, object]] = {}
table: list[str] = []
def scalar_value(raw: str) -> str:
value = raw.strip().split(" #", 1)[0].strip().rstrip(",}").strip()
return value.strip("'\"").strip()
def inline_value(raw: str, key: str) -> str | None:
match = re.search(rf"\b{re.escape(key)}\s*=\s*(\"[^\"]*\"|'[^']*'|[^,}}]+)", raw)
if match is None:
return None
return scalar_value(match.group(1))
for raw_line in source.splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("[") and line.endswith("]"):
table = [part.strip().strip("'\"") for part in line[1:-1].split(".")]
if len(table) >= 2 and table[0] == "mcp_servers":
servers.setdefault(table[1], {})
continue
if table == ["mcp_servers"] and "=" in line:
key, raw_value = line.split("=", 1)
server = servers.setdefault(key.strip().strip("'\""), {})
for field in ("command", "url"):
value = inline_value(raw_value, field)
if value is not None:
server[field] = value
continue
if len(table) >= 2 and table[0] == "mcp_servers" and "=" in line:
key, raw_value = line.split("=", 1)
key = key.strip().strip("'\"")
if key in {"command", "url"}:
servers.setdefault(table[1], {})[key] = scalar_value(raw_value)
return servers
def executable_candidate(candidate: str) -> bool:
"""Return whether a CLI candidate points to something runnable."""
value = candidate.strip()
if not value:
return False
if "/" not in value:
return shutil.which(value) is not None
path = Path(value).expanduser()
return path.is_file() and os.access(path, os.X_OK)
def codex_cli_ready(candidate: object) -> bool:
"""Return whether Codex runtime would have an executable launch candidate."""
if not isinstance(candidate, str):
return executable_candidate("codex")
value = candidate.strip()
if not value:
return executable_candidate("codex")
return executable_candidate(value)
def mcp_endpoint_ready(entry: object) -> bool:
"""Return whether the configured MCP endpoint can actually launch."""
if not isinstance(entry, dict):
return False
command = entry.get("command")
if isinstance(command, str) and command.strip():
return executable_candidate(command)
url = entry.get("url")
return isinstance(url, str) and bool(url.strip())
try:
config = yaml_mapping(config_path.read_text(encoding="utf-8"))
codex_source = codex_config_path.read_text(encoding="utf-8")
codex_config = tomllib.loads(codex_source) if tomllib is not None else {
"mcp_servers": toml_mcp_servers(codex_source)
}
except (OSError, ValueError):
raise SystemExit(1)
orchestrator = config.get("orchestrator") if isinstance(config, dict) else None
llm = config.get("llm") if isinstance(config, dict) else None
# Equivalent to [mcp_servers\.ouroboros], including quoted TOML key forms.
mcp_servers = codex_config.get("mcp_servers") if isinstance(codex_config, dict) else None
ouroboros_mcp = mcp_servers.get("ouroboros") if isinstance(mcp_servers, dict) else None
codex_cli_path = os.environ.get("OUROBOROS_CODEX_CLI_PATH")
if not codex_cli_path and isinstance(orchestrator, dict):
codex_cli_path = orchestrator.get("codex_cli_path")
ready = (
isinstance(orchestrator, dict)
and orchestrator.get("runtime_backend") == "codex"
and isinstance(llm, dict)
and llm.get("backend") == "codex"
and codex_cli_ready(codex_cli_path)
and mcp_endpoint_ready(ouroboros_mcp)
)
raise SystemExit(0 if ready else 1)
PY
then
CODEX_READY="true"
fi
Legacy Codex Model Migration
Some older Ouroboros configurations saved gpt-5 into all four stage-model
fields. That was a historical default, but it is now an explicit pin and would
stop Codex App/CLI model changes from taking effect. Do not silently rewrite a
possible user pin. Instead, when Codex is ready, detect that exact legacy
shape once before honoring the welcome-completed marker:
if ouroboros_python - "$HOME/.ouroboros/config.yaml" "$HOME/.ouroboros/prefs.json" <<'PY'
from __future__ import annotations
import json
import sys
from pathlib import Path
config_path, prefs_path = map(Path, sys.argv[1:])
def yaml_mapping(source: str) -> dict[str, dict[str, str]]:
parsed: dict[str, dict[str, str]] = {}
section: str | None = None
for raw_line in source.splitlines():
if not raw_line.strip() or raw_line.lstrip().startswith("#"):
continue
indent = len(raw_line) - len(raw_line.lstrip())
key, separator, raw_value = raw_line.strip().partition(":")
if not separator:
continue
value = raw_value.strip().split(" #", 1)[0].strip().strip("'\"")
if indent == 0:
section = key.strip("'\"")
parsed.setdefault(section, {})
elif section is not None:
parsed[section][key.strip("'\"")] = value
return parsed
try:
config = yaml_mapping(config_path.read_text(encoding="utf-8"))
except OSError:
raise SystemExit(1)
try:
prefs = json.loads(prefs_path.read_text(encoding="utf-8"))
except (OSError, ValueError):
prefs = {}
if not isinstance(prefs, dict):
prefs = {}
stage_values = (
config.get("clarification", {}).get("default_model"),
config.get("execution", {}).get("default_model"),
config.get("evaluation", {}).get("semantic_model"),
config.get("resilience", {}).get("reflect_model"),
)
legacy_gpt5 = all(value == "gpt-5" for value in stage_values)
partial_automatic_migration = (
any(value == "gpt-5" for value in stage_values)
and any(value == "default" for value in stage_values)
and all(value in {"gpt-5", "default"} for value in stage_values)
)
handled = prefs.get("codexModelMigration") in {"automatic-v1", "kept-gpt-5-v1"}
raise SystemExit(0 if (legacy_gpt5 or partial_automatic_migration) and not handled else 1)
PY
then
LEGACY_CODEX_MODEL_MIGRATION_REQUIRED="true"
fi
If CODEX_READY is true and LEGACY_CODEX_MODEL_MIGRATION_REQUIRED is true:
Use AskUserQuestion:
{
"questions": [{
"question": "현재 설정은 모든 단계에서 gpt-5를 고정해 두고 있어요. Codex에서 선택한 모델을 자동으로 사용하도록 바꿀까요?",
"header": "모델 설정",
"options": [
{
"label": "Codex 선택으로 전환하기 (권장)",
"description": "App이나 CLI에서 바꾼 모델을 모든 단계가 자동으로 따라가요"
},
{
"label": "gpt-5 고정 유지하기",
"description": "지금처럼 모든 단계를 gpt-5로 계속 실행해요"
}
],
"multiSelect": false
}]
}
- Codex 선택으로 전환하기: atomically rewrite the four legacy model pins on the current host:
ouroboros_python - "$HOME/.ouroboros/config.yaml" <<'PY'
from __future__ import annotations
import os
import sys
import tempfile
from pathlib import Path
path = Path(sys.argv[1])
original = path.read_text(encoding="utf-8")
replacements = {
("clarification", "default_model"): "default",
("execution", "default_model"): "default",
("evaluation", "semantic_model"): "default",
("resilience", "reflect_model"): "default",
}
seen: set[tuple[str, str]] = set()
section: str | None = None
output: list[str] = []
for line in original.splitlines(keepends=True):
stripped = line.strip()
indent = len(line) - len(line.lstrip())
key = stripped.split(":", 1)[0].strip("'\"") if ":" in stripped else ""
if indent == 0 and ":" in stripped:
section = key
target = (section or "", key)
if indent > 0 and target in replacements:
newline = "\n" if line.endswith("\n") else ""
prefix = line[:indent]
comment = ""
value_part = line.strip().split(":", 1)[1]
if " #" in value_part:
comment = " #" + value_part.split(" #", 1)[1].rstrip("\n")
output.append(f"{prefix}{key}: {replacements[target]}{comment}{newline}")
seen.add(target)
else:
output.append(line)
missing = set(replacements) - seen
if missing:
raise SystemExit(f"Cannot migrate Codex model pins; missing keys: {sorted(missing)}")
updated = "".join(output)
fd, tmp_name = tempfile.mkstemp(prefix=".config.yaml.", dir=str(path.parent))
try:
with os.fdopen(fd, "w", encoding="utf-8") as tmp:
tmp.write(updated)
tmp.flush()
os.fsync(tmp.fileno())
os.replace(tmp_name, path)
finally:
try:
os.unlink(tmp_name)
except FileNotFoundError:
pass
PY
default deliberately sends no model pin to Codex; it does not name a model
called "default". Confirm that the rewrite succeeded before recording the
decision. If this step is interrupted before the marker is written, the next
readiness check recognizes the partial gpt-5/default state and offers the
migration again.
- gpt-5 고정 유지하기: do not change
config.yaml.
For either completed choice, merge exactly one marker into
~/.ouroboros/prefs.json without deleting existing keys:
ouroboros_python - "automatic-v1" <<'PY'
import json, os, sys
path = os.path.expanduser('~/.ouroboros/prefs.json')
try:
prefs = json.load(open(path, encoding='utf-8'))
except Exception:
prefs = {}
if not isinstance(prefs, dict):
prefs = {}
prefs['codexModelMigration'] = sys.argv[1]
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w', encoding='utf-8') as f:
json.dump(prefs, f, indent=2)
f.write('\n')
PY
Pass kept-gpt-5-v1 instead of automatic-v1 for the keep choice. If welcome
was already completed, show a short confirmation and exit after recording this
decision; do not make the user answer the generic welcome question too.
If ALREADY_COMPLETED is true, SETUP_READY or CODEX_READY is true, AND no --force flag:
Use AskUserQuestion:
{
"questions": [{
"question": "Ouroboros welcome was already completed on $WELCOME_COMPLETED. What would you like to do?",
"header": "Welcome",
"options": [
{ "label": "Skip", "description": "Continue to work (recommended)" },
{ "label": "Re-run welcome", "description": "Go through the interactive onboarding again" }
],
"multiSelect": false
}]
}
- Skip: Mark as complete and exit
- Re-run welcome: Continue to Step 1 below
If the welcome was completed but neither SETUP_READY nor CODEX_READY is
true, bypass this completion prompt and continue to the Setup Gate below.
If --skip flag present:
- Merge
welcomeShown: true,welcomeCompleted: <current timestamp>, andwelcomeVersioninto~/.ouroboros/prefs.jsonwithout deleting existing keys:
ouroboros_python - <<'PY' import json, os from datetime import UTC, datetime path = os.path.expanduser('~/.ouroboros/prefs.json') os.makedirs(os.path.dirname(path), exist_ok=True) try: with open(path, encoding='utf-8') as f: prefs = json.load(f) if not isinstance(prefs, dict): prefs = {} except Exception: prefs = {} prefs.update({ 'welcomeShown': True, 'welcomeCompleted': datetime.now(UTC).isoformat(), 'welcomeVersion': '0.50.5', }) with open(path, 'w', encoding='utf-8') as f: json.dump(prefs, f, indent=2) f.write('\n') PY
- Show brief message:
Ouroboros welcome skipped. Run /ouroboros:welcome --force to re-run onboarding.
- Exit
---
### Setup Gate: First Use
Before showing the welcome banner, check whether setup is prepared for the
active runtime on this machine. A global `config.yaml` alone is not enough: it
must name a runtime this gate recognizes.
First accept a completed **Claude Code** setup. The marketplace plugin owns
its MCP capability; host-owned `~/.claude/mcp.json` is intentionally not part
of SDK setup readiness:
```bash
if ouroboros_python - "$HOME/.ouroboros/config.yaml" <<'PY'
from __future__ import annotations
import sys
from pathlib import Path
try:
import yaml
except ModuleNotFoundError:
yaml = None
config_path = Path(sys.argv[1])
def yaml_mapping(source: str) -> dict[str, dict[str, str]]:
"""Read only the top-level mapping scalars owned by this readiness gate."""
if yaml is not None:
loaded = yaml.safe_load(source) or {}
return loaded if isinstance(loaded, dict) else {}
parsed: dict[str, dict[str, str]] = {}
section: str | None = None
def scalar_value(raw: str) -> str:
return raw.strip().split(" #", 1)[0].strip().rstrip(",}").strip().strip("'\"")
def flow_mapping(raw: str) -> dict[str, str]:
value = raw.strip().split(" #", 1)[0].strip()
if not (value.startswith("{") and value.endswith("}")):
return {}
fields: dict[str, str] = {}
for part in value[1:-1].split(","):
key, separator, field_value = part.partition(":")
if separator:
fields[key.strip().strip("'\"")] = scalar_value(field_value)
return fields
for raw_line in source.splitlines():
if not raw_line.strip() or raw_line.lstrip().startswith("#"):
continue
indent = len(raw_line) - len(raw_line.lstrip())
key, separator, raw_value = raw_line.strip().partition(":")
if not separator:
continue
if indent == 0:
section = key.strip("'\"")
parsed[section] = flow_mapping(raw_value)
elif section is not None:
parsed.setdefault(section, {})[key.strip("'\"")] = scalar_value(raw_value)
return parsed
try:
config = yaml_mapping(config_path.read_text(encoding="utf-8"))
except (OSError, ValueError):
raise SystemExit(1)
orchestrator = config.get("orchestrator") if isinstance(config, dict) else None
llm = config.get("llm") if isinstance(config, dict) else None
# Existing YAML form: runtime_backend: claude. Parsing avoids assuming its order.
# The marketplace plugin owns its MCP capability. Host-owned
# ~/.claude/mcp.json is intentionally not part of SDK setup readiness.
ready = (
isinstance(orchestrator, dict)
and orchestrator.get("runtime_backend") in {"claude", "claude_mcp"}
and isinstance(llm, dict)
and llm.get("backend") == "claude"
)
raise SystemExit(0 if ready else 1)
PY
then
echo "SETUP_READY"
else
echo "SETUP_REQUIRED"
fi
If the Claude gate printed SETUP_READY, setup is complete: skip the Codex
gate below and continue directly to the welcome banner.
Otherwise check whether Codex is prepared:
CODEX_HOME_DIR="${CODEX_HOME:-$HOME/.codex}"
case "$CODEX_HOME_DIR" in
"~") CODEX_HOME_DIR="$HOME" ;;
"~/"*) CODEX_HOME_DIR="$HOME/${CODEX_HOME_DIR#"~/"}" ;;
esac
if ouroboros_python - "$HOME/.ouroboros/config.yaml" "$CODEX_HOME_DIR/config.toml" <<'PY'
from __future__ import annotations
import re
import os
import shutil
import sys
from pathlib import Path
try:
import tomllib
except ModuleNotFoundError: # Python 3.10 and earlier hosts
tomllib = None
try:
import yaml
except ModuleNotFoundError:
yaml = None
config_path, codex_config_path = map(Path, sys.argv[1:])
def yaml_mapping(source: str) -> dict[str, dict[str, str]]:
"""Read only the top-level mapping scalars owned by this readiness gate."""
if yaml is not None:
loaded = yaml.safe_load(source) or {}
return loaded if isinstance(loaded, dict) else {}
parsed: dict[str, dict[str, str]] = {}
section: str | None = None
def scalar_value(raw: str) -> str:
return raw.strip().split(" #", 1)[0].strip().rstrip(",}").strip().strip("'\"")
def flow_mapping(raw: str) -> dict[str, str]:
value = raw.strip().split(" #", 1)[0].strip()
if not (value.startswith("{") and value.endswith("}")):
return {}
fields: dict[str, str] = {}
for part in value[1:-1].split(","):
key, separator, field_value = part.partition(":")
if separator:
fields[key.strip().strip("'\"")] = scalar_value(field_value)
return fields
for raw_line in source.splitlines():
if not raw_line.strip() or raw_line.lstrip().startswith("#"):
continue
indent = len(raw_line) - len(raw_line.lstrip())
key, separator, raw_value = raw_line.strip().partition(":")
if not separator:
continue
if indent == 0:
section = key.strip("'\"")
parsed[section] = flow_mapping(raw_value)
elif section is not None:
parsed.setdefault(section, {})[key.strip("'\"")] = scalar_value(raw_value)
return parsed
def toml_mcp_servers(source: str) -> dict[str, dict[str, object]]:
"""Read MCP table membership when the host lacks the TOML standard library."""
servers: dict[str, dict[str, object]] = {}
table: list[str] = []
def scalar_value(raw: str) -> str:
value = raw.strip().split(" #", 1)[0].strip().rstrip(",}").strip()
return value.strip("'\"").strip()
def inline_value(raw: str, key: str) -> str | None:
match = re.search(rf"\b{re.escape(key)}\s*=\s*(\"[^\"]*\"|'[^']*'|[^,}}]+)", raw)
if match is None:
return None
return scalar_value(match.group(1))
for raw_line in source.splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
if line.startswith("[") and line.endswith("]"):
table = [part.strip().strip("'\"") for part in line[1:-1].split(".")]
if len(table) >= 2 and table[0] == "mcp_servers":
servers.setdefault(table[1], {})
continue
if table == ["mcp_servers"] and "=" in line:
key, raw_value = line.split("=", 1)
server = servers.setdefault(key.strip().strip("'\""), {})
for field in ("command", "url"):
value = inline_value(raw_value, field)
if value is not None:
server[field] = value
continue
if len(table) >= 2 and table[0] == "mcp_servers" and "=" in line:
key, raw_value = line.split("=", 1)
key = key.strip().strip("'\"")
if key in {"command", "url"}:
servers.setdefault(table[1], {})[key] = scalar_value(raw_value)
return servers
def executable_candidate(candidate: str) -> bool:
"""Return whether a CLI candidate points to something runnable."""
value = candidate.strip()
if not value:
return False
if "/" not in value:
return shutil.which(value) is not None
path = Path(value).expanduser()
return path.is_file() and os.access(path, os.X_OK)
def codex_cli_ready(candidate: object) -> bool:
"""Return whether Codex runtime would have an executable launch candidate."""
if not isinstance(candidate, str):
return executable_candidate("codex")
value = candidate.strip()
if not value:
return executable_candidate("codex")
return executable_candidate(value)
def mcp_endpoint_ready(entry: object) -> bool:
"""Return whether the configured MCP endpoint can actually launch."""
if not isinstance(entry, dict):
return False
command = entry.get("command")
if isinstance(command, str) and command.strip():
return executable_candidate(command)
url = entry.get("url")
return isinstance(url, str) and bool(url.strip())
try:
config = yaml_mapping(config_path.read_text(encoding="utf-8"))
codex_source = codex_config_path.read_text(encoding="utf-8")
codex_config = tomllib.loads(codex_source) if tomllib is not None else {
"mcp_servers": toml_mcp_servers(codex_source)
}
except (OSError, ValueError):
raise SystemExit(1)
orchestrator = config.get("orchestrator") if isinstance(config, dict) else None
llm = config.get("llm") if isinstance(config, dict) else None
# This is equivalent to checking [mcp_servers\.ouroboros], but TOML parsing
# also accepts a quoted "ouroboros" key and does not depend on table ordering.
mcp_servers = codex_config.get("mcp_servers") if isinstance(codex_config, dict) else None
ouroboros_mcp = mcp_servers.get("ouroboros") if isinstance(mcp_servers, dict) else None
codex_cli_path = os.environ.get("OUROBOROS_CODEX_CLI_PATH")
if not codex_cli_path and isinstance(orchestrator, dict):
codex_cli_path = orchestrator.get("codex_cli_path")
ready = (
isinstance(orchestrator, dict)
and orchestrator.get("runtime_backend") == "codex"
and isinstance(llm, dict)
and llm.get("backend") == "codex"
and codex_cli_ready(codex_cli_path)
and mcp_endpoint_ready(ouroboros_mcp)
)
raise SystemExit(0 if ready else 1)
PY
then
echo "CODEX_READY"
else
echo "CODEX_SETUP_REQUIRED"
fi
If neither gate reports ready (the Claude gate printed SETUP_REQUIRED and
the Codex gate printed CODEX_SETUP_REQUIRED), ask one concise question in
the user's language. This includes a user whose existing Ouroboros
configuration names a runtime neither gate recognizes. For a Korean
conversation, use:
{
"questions": [{
"question": "Ouroboros를 처음 사용하시네요. 시작하기 전에 실행 환경을 설정할까요?",
"header": "Ouroboros 시작하기",
"options": [
{
"label": "설정하고 시작하기 (권장)",
"description": "한 번만 설정하면 바로 사용할 수 있어요"
},
{
"label": "나중에",
"description": "지금은 기본 안내만 보고 나중에 설정할게요"
}
],
"multiSelect": false
}]
}
- 설정하고 시작하기: Run the setup command for the active host. In Codex
App or Codex CLI, use
ouroboros setup --runtime codexwhen the executable is installed. For a Marketplace-plugin-only install, useuvx --isolated --python '>=3.12' --from 'ouroboros-ai[mcp]' ouroboros setup --runtime codexinstead. In Claude Code, follow../setup/SKILL.md. Do not ask the user to copy a command when the current host can run it. - 나중에: Continue with the welcome flow, but do not claim that MCP-only execution features are ready.
After successful Codex setup, immediately ask:
{
"questions": [{
"question": "설정이 완료됐어요. 기본적으로 Codex에서 선택한 모델을 사용합니다. 모델은 언제든 나중에 바꿀 수 있어요.",
"header": "준비 완료",
"options": [
{
"label": "바로 시작하기 (권장)",
"description": "기본 모델로 바로 작업을 시작해요"
},
{
"label": "직접 모델 설정하기",
"description": "단계별로 모델을 바꾸거나 목록에 없는 모델 ID를 입력해 고정해요"
}
],
"multiSelect": false
}]
}
- 바로 시작하기: Continue to Step 1.
- 직접 모델 설정하기: Read and follow
../config/SKILL.md. On the user's local Codex App or Codex CLI this opens the settings UI in their browser at a temporarylocalhostaddress; it is not an external website. The UI offers Use Codex default model for the current Codex selection and Enter another model ID… for a deliberate stage pin. After the settings session ends, continue to Step 1.
For Claude Code, ../setup/SKILL.md presents the equivalent model
choice during its own completion flow. Do not show this Codex-specific question
a second time; continue to Step 1 after the Claude setup skill returns.
Do not show this gate again once Codex is ready. The normal settings UI remains
available later through ooo config, so a model choice made now is never
permanent.
Step 1: Welcome Banner
Display:
Welcome to Ouroboros!
The serpent that eats itself -- better every loop.
Most AI coding fails at the input, not the output.
Ouroboros fixes this by exposing hidden assumptions
BEFORE any code is written.
Interview -> Seed -> Execute -> Evaluate
^ |
+---- Evolutionary Loop -----+
Step 2: Persona Detection
AskUserQuestion:
{
"questions": [{
"question": "What brings you to Ouroboros?",
"header": "Welcome",
"options": [
{
"label": "New project idea",
"description": "I have a vague idea and want to crystallize it into a clear spec"
},
{
"label": "Tired of rewriting prompts",
"description": "AI keeps building the wrong thing because my requirements are unclear"
},
{
"label": "Just exploring",
"description": "Heard about Ouroboros and want to see what it does"
}
],
"multiSelect": false
}]
}
Give brief personalized response (1-2 sentences) based on choice.
Step 3: Advanced Runtime Check
Ordinary Claude setup uses the default [claude] Agent SDK profile on MCP 1.x.
It intentionally leaves host-owned ~/.claude/mcp.json untouched; do not
inspect or mutate that file as an onboarding health check. The dependency-free
worker is the explicit [claude-cli] profile used by an isolated MCP 2 process.
If the active runtime does not expose Ouroboros MCP tools, AskUserQuestion:
{
"questions": [{
"question": "Advanced MCP workflows require a host-managed MCP 2 launcher. What would you like to do?",
"header": "Runtime",
"options": [
{ "label": "Continue native (Recommended)", "description": "Use Claude-native interview, seed, evaluate, and unstuck workflows" },
{ "label": "Show MCP setup", "description": "See supported Codex, OpenCode, Kiro, Copilot, or Hermes setup commands" }
],
"multiSelect": false
}]
}
- Continue native: Continue to Step 4
- Show MCP setup: Explain that the Claude marketplace plugin or another
supported host setup owns the isolated
[mcp]launcher. Never combine[claude-sdk]with[mcp]. Then continue to Step 4.
Step 4: Quick Reference
Available Commands:
+---------------------------------------------------+
| Command | What It Does |
|-----------------|----------------------------------|
| ooo interview | Socratic Q&A -- expose hidden |
| | assumptions in your requirements |
| ooo seed | Crystallize answers into spec |
| ooo run | Execute with visual TUI |
| ooo evaluate | 3-stage verification |
| ooo unstuck | Lateral thinking when stuck |
| ooo config | Settings GUI: agents & models |
| ooo help | Full command reference |
+---------------------------------------------------+
Step 4: First Action
AskUserQuestion:
{
"questions": [{
"question": "What would you like to do first?",
"header": "Get started",
"options": [
{ "label": "Start a project", "description": "Run a Socratic interview on your idea right now" },
{ "label": "Try the tutorial", "description": "Interactive hands-on learning with a sample project" },
{ "label": "Read the docs", "description": "Full command reference and architecture overview" }
],
"multiSelect": false
}]
}
Based on choice:
- Start a project: Ask "What do you want to build?" → execute
../interview/SKILL.md - Try the tutorial: Execute
../tutorial/SKILL.md - Read the docs: Execute
../help/SKILL.md
Step 5: GitHub Star (Last Step)
Check gh availability first:
gh auth status &>/dev/null && echo "GH_OK" || echo "GH_MISSING"
If GH_OK AND star_asked not true:
AskUserQuestion:
{
"questions": [{
"question": "If you're enjoying Ouroboros, would you like to star it on GitHub?",
"header": "Community",
"options": [
{ "label": "Star on GitHub", "description": "Takes 1 second -- helps the project grow" },
{ "label": "Maybe later", "description": "Skip for now" }
],
"multiSelect": false
}]
}
- Star on GitHub:
gh api -X PUT /user/starred/Q00/ouroboros - Both choices: merge the welcome completion fields into
~/.ouroboros/prefs.jsonwithout deleting existing keys. Setstar_asked: trueafter either star prompt choice so the star prompt is not repeated:
ouroboros_python - <<'PY' import json, os from datetime import UTC, datetime path = os.path.expanduser('~/.ouroboros/prefs.json') os.makedirs(os.path.dirname(path), exist_ok=True) try: with open(path, encoding='utf-8') as f: prefs = json.load(f) if not isinstance(prefs, dict): prefs = {} except Exception: prefs = {} prefs.update({ 'star_asked': True, 'welcomeShown': True, 'welcomeCompleted': datetime.now(UTC).isoformat(), 'welcomeVersion': '0.50.5', }) with open(path, 'w', encoding='utf-8') as f: json.dump(prefs, f, indent=2) f.write('\n') PY
**If `GH_MISSING` or `star_asked` is true:**
Merge the welcome completion fields into `~/.ouroboros/prefs.json` without deleting existing keys:
```bash
ouroboros_python - <<'PY'
import json, os
from datetime import UTC, datetime
path = os.path.expanduser('~/.ouroboros/prefs.json')
os.makedirs(os.path.dirname(path), exist_ok=True)
try:
with open(path, encoding='utf-8') as f:
prefs = json.load(f)
if not isinstance(prefs, dict):
prefs = {}
except Exception:
prefs = {}
prefs.update({
'welcomeShown': True,
'welcomeCompleted': datetime.now(UTC).isoformat(),
'welcomeVersion': '0.50.5',
})
with open(path, 'w', encoding='utf-8') as f:
json.dump(prefs, f, indent=2)
f.write('\n')
PY
Completion Message
Ouroboros Setup Complete!
MAGIC KEYWORDS (optional shortcuts):
Just include these naturally in your request:
| Keyword | Effect | Example |
|---------|--------|---------|
| interview | Socratic Q&A | "interview me about my app idea" |
| seed | Crystallize spec | "seed the requirements" |
| evaluate | 3-stage check | "evaluate this implementation" |
| stuck | Lateral thinking | "I'm stuck on the auth flow" |
REAL-TIME MONITORING (TUI):
When running ooo run or ooo evolve, open a separate terminal:
uvx --python '>=3.12' --from 'ouroboros-ai[tui]' ouroboros tui monitor
Press 1-4 to switch screens (Dashboard, Execution, Logs, Debug).
READY TO BUILD:
- ooo interview "your project idea"
- ooo tutorial # Interactive learning
- ooo help # Full reference
Prefs File Structure
~/.ouroboros/prefs.json:
{
"welcomeShown": true,
"welcomeCompleted": "2025-02-23T15:30:00+09:00",
"welcomeVersion": "0.50.5",
"star_asked": true
}
RFC #1392 State Breadcrumb Footer
Your final response MUST end with exactly one breadcrumb footer line:
◆ <current state> → next: <recommended action>
Derive <current state> from live session state via ouroboros_session_status when that MCP projection is available; otherwise derive it from this skill's actual outcome. Never use a linear Step N of M footer because Ouroboros is an evolutionary loop. When the next action is genuinely a choice, list 2-3 honest options in the next: clause. The breadcrumb line must be the last line of the response.
有意识地管理
安装与管理
前置条件与目标 Profile
目标: codex Profile, claude-code Profile
交付方式: Skill 文件 — https://raw.githubusercontent.com/Q00/ouroboros/03714ba446186423dcb46e25d12bd19c3a2e82f6/skills/welcome/SKILL.md。
兼容性与访问范围
Python 3.12+ or uv; includes Codex and Claude readiness checks.: Not declared in supplied evidence。
风险事实
证据与编辑审查Manifest、Bundle patch、分发与新鲜度
不可变证据
审查状态与源码活动
在核对来源内容和不可变发布记录后,已由人工批准发布。AI 参与了内容草稿生成,最终发布决定由人工完成。
人工审查于 2026/9/5 UTC 17:22。GitHub 事实核对日期: 2026/9/5 UTC 16:31。
自当前证据基线以来,没有记录到重要源码变化。