optimize docs

This commit is contained in:
thanhnv
2026-07-23 23:30:43 +07:00
parent a6c3a5d02f
commit 2ca1eabca9
7 changed files with 390 additions and 201 deletions
+252 -44
View File
@@ -26,10 +26,12 @@ launcher) or --harness.
from __future__ import annotations
import argparse
from contextlib import contextmanager
import json
import os
import re
import shutil
import stat
import subprocess
import sys
import tempfile
@@ -52,6 +54,48 @@ VSCODE_EXTENSION_IDS = {
"fpt-casan.casan-governed-chat",
),
}
PROMPT_MARKER_START = "<!-- CASAN_PROMPT_ENFORCEMENT_START -->"
PROMPT_MARKER_END = "<!-- CASAN_PROMPT_ENFORCEMENT_END -->"
@contextmanager
def owner_writable(path):
"""Temporarily make an owner-controlled path writable, then restore modes.
Some existing shells intentionally commit `.claude`/`.github` directories
as 0555. CASAN may add its config there only when the current user owns the
path; it never escalates privileges or leaves permissions broadened.
"""
changed = []
candidates = []
if os.path.exists(path):
candidates.append(path)
parent = os.path.dirname(path)
while parent and not os.path.exists(parent):
parent = os.path.dirname(parent)
if parent and parent not in candidates:
candidates.append(parent)
try:
for candidate in candidates:
info = os.stat(candidate)
mode = stat.S_IMODE(info.st_mode)
if mode & stat.S_IWUSR:
continue
getuid = getattr(os, "getuid", None)
if getuid is not None and info.st_uid != getuid():
raise PermissionError(
"CASAN cannot write %s: current user does not own %s" %
(path, candidate))
os.chmod(candidate, mode | stat.S_IWUSR)
changed.append((candidate, mode))
yield
finally:
for candidate, mode in reversed(changed):
try:
os.chmod(candidate, mode)
except OSError:
pass
def now_iso():
@@ -65,11 +109,12 @@ def devkit_root():
def _copy_if_absent(src, dst, created_rel, target, created):
if not os.path.exists(src) or os.path.exists(dst):
return False
os.makedirs(os.path.dirname(dst), exist_ok=True)
with open(src, "rb") as fh:
data = fh.read()
with open(dst, "wb") as fh:
fh.write(data)
with owner_writable(dst):
os.makedirs(os.path.dirname(dst), exist_ok=True)
with open(dst, "wb") as fh:
fh.write(data)
created.append(os.path.relpath(dst, target))
return True
@@ -148,17 +193,18 @@ def compute_harness_hash(harness):
def _write(path, text, backups):
if os.path.exists(path):
bak = path + ".casan-bak"
if not os.path.exists(bak):
with open(path, "r", encoding="utf-8", errors="replace") as fh:
old = fh.read()
with open(bak, "w", encoding="utf-8") as fh:
fh.write(old)
backups.append(bak)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as fh:
fh.write(text)
with owner_writable(path):
if os.path.exists(path):
bak = path + ".casan-bak"
if not os.path.exists(bak):
with open(path, "r", encoding="utf-8", errors="replace") as fh:
old = fh.read()
with open(bak, "w", encoding="utf-8") as fh:
fh.write(old)
backups.append(bak)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as fh:
fh.write(text)
def _copy_template(src, dst, backups):
@@ -183,8 +229,9 @@ def _backup_once(path, backups):
if os.path.exists(path) and not os.path.exists(bak):
with open(path, "r", encoding="utf-8", errors="replace") as fh:
old = fh.read()
with open(bak, "w", encoding="utf-8") as fh:
fh.write(old)
with owner_writable(bak):
with open(bak, "w", encoding="utf-8") as fh:
fh.write(old)
backups.append(bak)
@@ -271,10 +318,11 @@ def merge_json_hooks(target_file, template_file, marker, backups):
doc[k] = v
changed = (not existed) or before != json.dumps(doc, ensure_ascii=False, sort_keys=True)
if changed:
_backup_once(target_file, backups)
os.makedirs(os.path.dirname(target_file), exist_ok=True)
with open(target_file, "w", encoding="utf-8") as fh:
fh.write(json.dumps(doc, ensure_ascii=False, indent=2) + "\n")
with owner_writable(target_file):
_backup_once(target_file, backups)
os.makedirs(os.path.dirname(target_file), exist_ok=True)
with open(target_file, "w", encoding="utf-8") as fh:
fh.write(json.dumps(doc, ensure_ascii=False, indent=2) + "\n")
return ("created" if not existed else "merged") if changed else "unchanged"
@@ -285,9 +333,10 @@ def remove_json_hooks(target_file, marker, backups):
doc = _load_json_or(target_file, None)
if not isinstance(doc, dict) or not _remove_marker_hooks(doc, marker):
return "unchanged"
_backup_once(target_file, backups)
with open(target_file, "w", encoding="utf-8") as fh:
fh.write(json.dumps(doc, ensure_ascii=False, indent=2) + "\n")
with owner_writable(target_file):
_backup_once(target_file, backups)
with open(target_file, "w", encoding="utf-8") as fh:
fh.write(json.dumps(doc, ensure_ascii=False, indent=2) + "\n")
return "removed"
@@ -326,15 +375,133 @@ def clean_legacy_codex_config(target_file, backups):
r"(?ms)\n?\[hooks\]\nenabled = true\nproject_hooks = true\n(?=\s*$)",
"\n", cleaned)
cleaned = cleaned.strip() + ("\n" if cleaned.strip() else "")
_backup_once(target_file, backups)
if cleaned:
with open(target_file, "w", encoding="utf-8") as fh:
fh.write(cleaned)
else:
os.unlink(target_file)
with owner_writable(target_file):
_backup_once(target_file, backups)
if cleaned:
with open(target_file, "w", encoding="utf-8") as fh:
fh.write(cleaned)
else:
os.unlink(target_file)
return "cleaned"
def is_casan_source_hub(target):
"""Distinguish this product's source checkout from an adopted vendored app.
Older DevKit adoption copied ``packages/casan-harness`` into an application.
That directory alone therefore does not make the application a CASAN source
hub. The source checkout also carries the DevKit source, release manifest,
and root installer.
"""
required = (
os.path.join("packages", "casan-harness", "scripts", "bash",
"casan-harness.sh"),
os.path.join("packages", "casan-devkit", "casan-init.py"),
os.path.join("packaging", "levels.json"),
"install.sh",
)
return all(os.path.isfile(os.path.join(target, item)) for item in required)
def _hybrid_prompt_block(clients):
enabled = ", ".join(clients) if clients else "none"
return """<!-- CASAN_PROMPT_ENFORCEMENT_START -->
## CASAN IDE governance — mandatory
- `.casan/config.json` is the source of truth for enabled integrations (currently: %s).
- Claude Code and Codex normal prompts are governed only when their integration is enabled and the project hooks are loaded; Codex additionally requires `/hooks` trust review.
- GitHub Copilot built-in chat is not globally intercepted. Use the explicit `@casan` participant for a CASAN-owned Copilot turn.
- A retained `bin/casan-chat` entrypoint remains a compatible governed route, but it is no longer the only supported prompt boundary.
- Do not claim CASAN certification without the matching trace/evidence, and do not bypass a denied or degraded decision.
<!-- CASAN_PROMPT_ENFORCEMENT_END -->""" % enabled
def _replace_managed_prompt_block(path, replacement, backups):
if not os.path.isfile(path):
return "absent"
with open(path, "r", encoding="utf-8", errors="replace") as fh:
current = fh.read()
pattern = re.compile(
re.escape(PROMPT_MARKER_START) + r".*?" +
re.escape(PROMPT_MARKER_END), re.DOTALL)
if not pattern.search(current):
return "unmanaged"
updated = pattern.sub(replacement, current)
if updated == current:
return "unchanged"
with owner_writable(path):
_backup_once(path, backups)
with open(path, "w", encoding="utf-8") as fh:
fh.write(updated.rstrip() + "\n")
return "migrated"
def _legacy_instruction_conflicts(target):
"""Report unmarked legacy prose that init must not silently rewrite."""
conflicts = []
phrases = (
"The supported prompt boundary is `bin/casan-chat`",
"CASAN Core が intercept できない",
"Direct Claude, ChatGPT, Codex or Copilot UI prompts are outside",
)
for relative in ("AGENTS.md", "CLAUDE.md",
os.path.join(".github", "copilot-instructions.md")):
path = os.path.join(target, relative)
if not os.path.isfile(path):
continue
with open(path, "r", encoding="utf-8", errors="replace") as fh:
current = fh.read()
unmanaged = re.sub(
re.escape(PROMPT_MARKER_START) + r".*?" +
re.escape(PROMPT_MARKER_END), "", current, flags=re.DOTALL)
if any(phrase in unmanaged for phrase in phrases):
conflicts.append(relative.replace(os.sep, "/"))
return conflicts
def migrate_vendored_prompt_contract(target, clients, backups):
"""Migrate only CASAN-owned legacy files; retain all project-owned content."""
replacement = _hybrid_prompt_block(clients)
instruction_files = {}
for relative in ("AGENTS.md", "CLAUDE.md",
os.path.join(".github", "copilot-instructions.md")):
instruction_files[relative.replace(os.sep, "/")] = (
_replace_managed_prompt_block(
os.path.join(target, relative), replacement, backups))
policy_path = os.path.join(target, ".casan", "prompt-policy.json")
policy_status = "absent"
if os.path.isfile(policy_path):
policy = _load_json_or(policy_path, None)
if isinstance(policy, dict):
before = json.dumps(policy, ensure_ascii=False, sort_keys=True)
policy["direct_external_ui"] = "client_dependent_see_config"
policy["agentic_clients"] = clients
policy["hybrid_adoption"] = True
if before != json.dumps(policy, ensure_ascii=False, sort_keys=True):
with owner_writable(policy_path):
_backup_once(policy_path, backups)
with open(policy_path, "w", encoding="utf-8") as fh:
fh.write(json.dumps(
policy, ensure_ascii=False, indent=2) + "\n")
policy_status = "migrated"
else:
policy_status = "unchanged"
else:
policy_status = "unreadable"
return {
"detected": True,
"vendored_harness": "retained_for_compatibility",
"managed_instruction_blocks": instruction_files,
"prompt_policy": policy_status,
"manual_review": _legacy_instruction_conflicts(target),
"note": (
"Existing .claude agents/skills/commands, .github content, CI, "
"and the vendored harness were preserved."),
}
def select_clients(values, interactive):
"""Normalize repeatable/comma-separated selections.
@@ -403,10 +570,11 @@ def merge_vscode_recommendations(target_file, clients, backups):
changed = (not existed and bool(doc)) or before != json.dumps(
doc, ensure_ascii=False, sort_keys=True)
if changed:
_backup_once(target_file, backups)
os.makedirs(os.path.dirname(target_file), exist_ok=True)
with open(target_file, "w", encoding="utf-8") as fh:
fh.write(json.dumps(doc, ensure_ascii=False, indent=2) + "\n")
with owner_writable(target_file):
_backup_once(target_file, backups)
os.makedirs(os.path.dirname(target_file), exist_ok=True)
with open(target_file, "w", encoding="utf-8") as fh:
fh.write(json.dumps(doc, ensure_ascii=False, indent=2) + "\n")
return ("created" if not existed else "merged") if changed else "unchanged"
@@ -461,17 +629,18 @@ def cmd_init(args):
sys.stderr.write("casan init: target is not a directory: %s\n" % target)
return 66
# Guardrail: refuse to adopt a CASAN SOURCE HUB into itself. Installing the
# Plan-20 PreToolUse hook into the CASAN repo would block the very agent
# developing CASAN (no admission => deny). Detect the hub by the harness
# source living inside the target. Override with --force for the rare
# intentional case.
# Guardrail: refuse to adopt the CASAN PRODUCT SOURCE HUB into itself.
# A vendored application from the previous DevKit model also contains
# packages/casan-harness, so that directory by itself is not sufficient to
# identify a source hub. Such applications are migrated in place below.
hub_marker = os.path.join(target, "packages", "casan-harness", "scripts", "bash", "casan-harness.sh")
if os.path.exists(hub_marker) and not args.force:
source_hub = is_casan_source_hub(target)
vendored_harness = os.path.isfile(hub_marker) and not source_hub
if source_hub and not args.force:
sys.stderr.write(
"casan init: target looks like a CASAN SOURCE HUB (%s exists) — refusing to "
"casan init: target is a CASAN SOURCE HUB — refusing to "
"adopt CASAN into itself (the Plan-20 hooks would block your own agent). "
"Use --force only if you really mean to.\n" % os.path.relpath(hub_marker, target))
"Use --force only if you really mean to.\n")
return 65
project = args.project or re.sub(r"[^a-z0-9-]", "-", os.path.basename(target).lower()).strip("-")
@@ -571,8 +740,10 @@ def cmd_init(args):
# ── .specify/ state root marker ──
specify = os.path.join(target, ".specify")
os.makedirs(os.path.join(specify, "state"), exist_ok=True)
os.makedirs(os.path.join(specify, "logs"), exist_ok=True)
with owner_writable(os.path.join(specify, "state")):
os.makedirs(os.path.join(specify, "state"), exist_ok=True)
with owner_writable(os.path.join(specify, "logs")):
os.makedirs(os.path.join(specify, "logs"), exist_ok=True)
gi = os.path.join(specify, ".gitignore")
if not os.path.exists(gi):
_write(gi, "# CASAN runtime state — do not commit\nlogs/\nstate/\n", backups); created_add(gi)
@@ -626,6 +797,12 @@ def cmd_init(args):
{"status": "skipped", "reason": "vscode-copilot_not_selected"}
)
legacy_migration = (
migrate_vendored_prompt_contract(target, clients, backups)
if vendored_harness else
{"detected": False}
)
# ── Level 2 (devkit) adoption extras: CI workflow + domain-pack scaffold.
# These are the real difference between L1 (gate/hooks only) and L2 (full
# adoption). Copied only if absent — never clobber the project's own files.
@@ -665,10 +842,16 @@ def cmd_init(args):
"created": created,
"hook_merges": merges,
"vscode_extension": vscode_install,
"legacy_migration": legacy_migration,
"level_extras": level_extras,
"note": ("harness NOT copied into repo (hybrid model); selected client hooks "
"MERGED and unselected CASAN hooks removed; run `casan doctor`"),
}, ensure_ascii=False, indent=2))
if legacy_migration.get("manual_review"):
sys.stderr.write(
"casan init: WARNING — legacy CASAN prose outside managed markers "
"still needs review in: %s\n" %
", ".join(legacy_migration["manual_review"]))
if preview:
sys.stderr.write("casan init: NOTE — Level 3 (platform) is a PREVIEW SERVICE (Control "
"Panel/Dashboard), adopted by DEPLOYING it, not by repo config. "
@@ -825,6 +1008,16 @@ def cmd_doctor(args):
"warnings": [],
}
ready = integrity_ok and bootstrap_ok
vendored_harness = os.path.isfile(os.path.join(
target, "packages", "casan-harness", "scripts", "bash",
"casan-harness.sh"))
legacy_conflicts = _legacy_instruction_conflicts(target)
checks["legacy_migration"] = {
"vendored_harness_present": vendored_harness,
"vendored_harness_status": (
"retained_for_compatibility" if vendored_harness else "not_present"),
"instruction_files_requiring_review": legacy_conflicts,
}
code = shutil.which("code")
installed_extensions = set()
@@ -888,6 +1081,15 @@ def cmd_doctor(args):
checks["warnings"].append(
"Only prompts explicitly sent to @casan use the CASAN-owned Copilot route; "
"built-in Copilot chat is not globally intercepted.")
if vendored_harness:
checks["warnings"].append(
"A legacy vendored packages/casan-harness is retained. Remove it only "
"after CI, bin scripts, and domain smoke tests use the global harness.")
if legacy_conflicts:
checks["warnings"].append(
"Legacy prompt-boundary prose outside CASAN-managed markers conflicts "
"with Plan-20 hooks and needs manual review: %s." %
", ".join(legacy_conflicts))
checks["status"] = "ready" if ready else "not_ready"
print(json.dumps(checks, ensure_ascii=False, indent=2))
return 0 if ready else 2
@@ -938,7 +1140,13 @@ def main(argv=None):
args = parser.parse_args(argv)
if args.cmd == "init":
return cmd_init(args)
try:
return cmd_init(args)
except PermissionError as error:
sys.stderr.write(
"casan init: permission denied while merging project config: "
"%s\n" % error)
return 77
if args.cmd == "verify":
return cmd_verify(args)
if args.cmd == "level":