feat: support Codex and Claude local surfaces
This commit is contained in:
@@ -60,10 +60,35 @@ VSCODE_EXTENSION_IDS = {
|
||||
PROMPT_MARKER_START = "<!-- CASAN_PROMPT_ENFORCEMENT_START -->"
|
||||
PROMPT_MARKER_END = "<!-- CASAN_PROMPT_ENFORCEMENT_END -->"
|
||||
CLIENT_LABELS = {
|
||||
"claude": "Claude Code",
|
||||
"codex": "Codex",
|
||||
"claude": "Claude Code (CLI + VS Code + JetBrains)",
|
||||
"codex": "Codex local (desktop + CLI + IDE)",
|
||||
"vscode-copilot": "VS Code / @casan",
|
||||
}
|
||||
CLIENT_SURFACES = {
|
||||
"claude": {
|
||||
"supported_local": [
|
||||
"claude-code-cli",
|
||||
"claude-code-vscode",
|
||||
"claude-code-jetbrains",
|
||||
],
|
||||
"not_covered": ["claude-desktop", "claude-web"],
|
||||
"contract": "shared_claude_code_project_settings",
|
||||
},
|
||||
"codex": {
|
||||
"supported_local": [
|
||||
"codex-desktop-local",
|
||||
"codex-cli",
|
||||
"codex-ide-extension-local",
|
||||
],
|
||||
"not_covered": ["codex-cloud", "codex-web"],
|
||||
"contract": "shared_codex_local_project_hooks",
|
||||
},
|
||||
"vscode-copilot": {
|
||||
"supported_local": ["vscode-copilot-explicit-at-casan"],
|
||||
"not_covered": ["github-copilot-built-in-chat"],
|
||||
"contract": "casan_owned_explicit_route",
|
||||
},
|
||||
}
|
||||
RUNTIME_MODES = ("managed", "vendored")
|
||||
|
||||
|
||||
@@ -131,7 +156,7 @@ def _render_init(result):
|
||||
print(_color("1", "Next steps"))
|
||||
print(" 1. Run `casan doctor`")
|
||||
if "codex" in result["clients"]:
|
||||
print(" 2. In Codex, open `/hooks` and trust this project's hook.")
|
||||
print(" 2. In a local Codex client, open `/hooks` and trust this project's hook.")
|
||||
|
||||
|
||||
def _render_verify(result):
|
||||
@@ -173,6 +198,9 @@ def _render_doctor(result):
|
||||
for client, item in result["client_checks"].items():
|
||||
label = CLIENT_LABELS.get(client, client)
|
||||
print(" %s %s" % (_mark(item.get("ready", False)), label))
|
||||
surfaces = item.get("supported_surfaces") or []
|
||||
if surfaces:
|
||||
print(" Surfaces: %s" % ", ".join(surfaces))
|
||||
reason = item.get("smoke", {}).get("reason")
|
||||
if reason and not item.get("ready"):
|
||||
print(" %s" % reason)
|
||||
@@ -903,8 +931,8 @@ def select_clients(values, interactive):
|
||||
while True:
|
||||
sys.stderr.write(
|
||||
"\nClient integrations\n\n"
|
||||
" 1) Claude Code (CLI + official VS Code extension)\n"
|
||||
" 2) Codex (CLI + official VS Code extension)\n"
|
||||
" 1) Claude Code (CLI + VS Code + JetBrains)\n"
|
||||
" 2) Codex local (desktop app + CLI + IDE extension)\n"
|
||||
" 3) GitHub Copilot in VS Code via explicit @casan route\n\n"
|
||||
"Select clients (comma-separated) [1,2]: ")
|
||||
sys.stderr.flush()
|
||||
@@ -1108,7 +1136,7 @@ def cmd_init(args):
|
||||
# ── .casan/config.json ──
|
||||
cfg_dir = os.path.join(target, ".casan")
|
||||
cfg = {
|
||||
"schema_version": "21.1",
|
||||
"schema_version": "21.2",
|
||||
"project_id": project,
|
||||
"created_at": now_iso(),
|
||||
"enforcement_mode": args.mode,
|
||||
@@ -1121,6 +1149,13 @@ def cmd_init(args):
|
||||
if "vscode-copilot" in clients else "disabled",
|
||||
"vscode-native-copilot": "unsupported_global_interception",
|
||||
},
|
||||
"client_surfaces": {
|
||||
client: {
|
||||
"enabled": client in clients,
|
||||
**surface,
|
||||
}
|
||||
for client, surface in CLIENT_SURFACES.items()
|
||||
},
|
||||
"harness_version": version,
|
||||
"adoption_model": (
|
||||
"managed-global" if runtime_mode == "managed"
|
||||
@@ -1511,7 +1546,13 @@ def cmd_doctor(args):
|
||||
line.strip().lower() for line in listed.stdout.splitlines() if line.strip()}
|
||||
|
||||
for client in clients:
|
||||
item = {"configured": True}
|
||||
surface = CLIENT_SURFACES.get(client, {})
|
||||
item = {
|
||||
"configured": True,
|
||||
"supported_surfaces": list(surface.get("supported_local", [])),
|
||||
"not_covered_surfaces": list(surface.get("not_covered", [])),
|
||||
"surface_contract": surface.get("contract"),
|
||||
}
|
||||
if client == "claude":
|
||||
commands = _commands_in_hooks(
|
||||
os.path.join(target, ".claude", "settings.json"))
|
||||
|
||||
@@ -6,6 +6,10 @@ installer does this). It wires the four Claude Code lifecycle hooks
|
||||
bridge so a developer can type prompts normally while every certified turn still
|
||||
carries an H1→H7 trace and an H6 record.
|
||||
|
||||
Claude Code shares these project settings across its CLI, official VS Code
|
||||
extension, and JetBrains integration. Claude Desktop and claude.ai are separate
|
||||
products and are not covered by this local project-hook integration.
|
||||
|
||||
- The hook command resolves the repo root via `$CLAUDE_PROJECT_DIR`, so **no
|
||||
machine-specific path is committed**.
|
||||
- It invokes `.casan/casan-hook.py`. That bootstrap reads project config,
|
||||
|
||||
@@ -5,6 +5,11 @@ handler calls `.casan/casan-hook.py`, which resolves and verifies the pinned
|
||||
global harness before dispatching the Codex adapter. No custom `[casan]` TOML
|
||||
keys are required.
|
||||
|
||||
This one integration covers the local Codex runtime across the desktop app,
|
||||
CLI, and IDE extension. It does not claim coverage for Codex Cloud/Web, where a
|
||||
repo-local process and its `.casan/casan-hook.py` bootstrap are not the local
|
||||
execution boundary.
|
||||
|
||||
Important Codex-specific facts (Spike-20 §4.2):
|
||||
|
||||
- **Trust review is mandatory.** Codex loads project-local hooks only after the
|
||||
|
||||
@@ -151,6 +151,23 @@ PROJ="$WORK/proj/my-app"; mkdir -p "$PROJ"; echo '{"name":"x"}' > "$PROJ/package
|
||||
for f in .casan/config.json .casan/version.lock .casan/agentic.env .casan/casan-hook.py .claude/settings.json .codex/hooks.json .vscode/extensions.json .specify/.gitignore; do
|
||||
[[ -f "$PROJ/$f" ]] && pass "init wrote $f" || fail "init missing $f"
|
||||
done
|
||||
python3 - "$PROJ/.casan/config.json" <<'PY' \
|
||||
&& pass "config declares supported and excluded client surfaces" \
|
||||
|| fail "config client surface contract is incomplete"
|
||||
import json
|
||||
import sys
|
||||
|
||||
d = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
assert d["schema_version"] == "21.2"
|
||||
assert d["client_surfaces"]["claude"]["supported_local"] == [
|
||||
"claude-code-cli", "claude-code-vscode", "claude-code-jetbrains",
|
||||
]
|
||||
assert d["client_surfaces"]["codex"]["supported_local"] == [
|
||||
"codex-desktop-local", "codex-cli", "codex-ide-extension-local",
|
||||
]
|
||||
assert "claude-desktop" in d["client_surfaces"]["claude"]["not_covered"]
|
||||
assert "codex-cloud" in d["client_surfaces"]["codex"]["not_covered"]
|
||||
PY
|
||||
if [[ -d "$PROJ/packages/casan-harness" ]]; then fail "harness was copied into the repo (should not be)"; else pass "harness NOT copied into repo (hybrid model)"; fi
|
||||
if grep -R -q 'packages/casan-harness/adapters' "$PROJ/.claude/settings.json" "$PROJ/.codex/hooks.json"; then fail "generated hooks still target a repo-local harness"; else pass "generated hooks target the stable project bootstrap"; fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user