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
+36 -148
View File
@@ -17,7 +17,7 @@ copy toàn bộ CASAN hoặc dựng lại project shell.
- [CASAN Project Shell production](docs/guides/CASAN_PROJECT_SHELL_PRODUCTION.md)
- [Requirements](#requirements)
- [Input for Flow](#input-for-flow)
- [Output for FLow](#output-for-flow)
- [Output for Flow](#output-for-flow)
- [Full AI-SDLC Flow](#full-ai-sdlc-flow)
- [Flow Steps](#flow-steps)
- [Flow Diagram](#flow-diagram)
@@ -28,7 +28,6 @@ copy toàn bộ CASAN hoặc dựng lại project shell.
- [AI-SDLC Usage Guide](#ai-sdlc-usage-guide)
- [Directory Structure](#directory-structure)
- [Directory Details](#directory-details)
- [OKR Web Application — Quick Start](#okr-web-application--quick-start)
## CASAN Quick Start
@@ -100,6 +99,15 @@ Mặc định production là `--mode enforce`. Chỉ dùng `--mode observe` cho
Nếu ứng dụng tự gọi LLM API qua backend riêng, ngoài Claude Code/Codex/Copilot,
luồng đó không tự động đi qua project hooks và cần adapter CASAN riêng.
Với project đã adopt CASAN theo mô hình cũ và đang chứa
`packages/casan-harness`, init nhận diện đây là vendored application thay vì
source hub:
- Không cần `--force`.
- Không xóa harness, scripts, workflow hoặc evidence cũ.
- Tự nâng cấp các block nằm giữa marker `CASAN_PROMPT_ENFORCEMENT_START/END`.
- Báo rõ file nào còn prose legacy ngoài marker cần review thủ công.
### 3. Hành vi chat theo client
| Client đã enable | Prompt đi qua CASAN | Onboarding bắt buộc |
@@ -143,9 +151,9 @@ Apply AI to the SDLC process to automate and optimize the creation of software p
## Input for Flow
- `apps/okr/domain/input/okr-requirement.md` — Raw requirements from customers
- `docs/technical_architecture.md` — Technical stack decisions and constraints
- `apps/okr/domain/input/change-request/` — Change requests (e.g., `change-0406.md`)
- `apps/<project-id>/domain/input/requirement.md` — project requirements
- `apps/<project-id>/domain/input/architecture.md` — architecture and constraints
- Project-owned input, reference, agent, and skill directories remain untouched
## Output for Flow
@@ -155,7 +163,7 @@ Apply AI to the SDLC process to automate and optimize the creation of software p
| **AI Agent Logs** | `docs/output/output_logs/` | Execution logs from AI agents |
| **SRS-Systems** | `docs/output/srs-systems/` | System-wide SRS (generated once during input clarification phase, not part of the main flow steps) |
| **Spec-Kit Artifacts** | `specs/[FEATURE_NAME]/` | Feature artifacts generated by Spec-Kit agents (`spec.md`, `plan.md`, `tasks.md`, contracts, checklists) |
| **Source Code** | `apps/okr/backend/` + `apps/okr/frontend/` | Application source code (NestJS backend + React frontend) |
| **Source Code** | Project-defined locations | CASAN does not impose an application framework or source layout |
---
@@ -280,7 +288,7 @@ The entire flow is orchestrated by **`casan.pipeline`**, which executes all step
## AI-SDLC Usage Guide
1. **Adopt governance**: Run `casan init`, select the IDE clients, complete any trust/VSIX onboarding, then run `casan doctor`
2. **Prepare Input**: Place raw requirements in `apps/okr/domain/input/okr-requirement.md` and tech stack in `docs/technical_architecture.md`
2. **Prepare Input**: Place requirements and architecture under `apps/<project-id>/domain/input/`
3. **Configuration**: Set up the project constitution first, keep specifications focused on what and why, and only decide technical stack where the input explicitly requires it
4. **(One-time) Generate System SRS**: Run `casan.srs` to generate system-wide SRS → `docs/output/srs-systems/`
5. **Run AI Flow**: Invoke `casan.pipeline` to execute the full 13-step flow automatically, with strict TDD enforced across specification, planning, task generation, and implementation
@@ -289,7 +297,7 @@ The entire flow is orchestrated by **`casan.pipeline`**, which executes all step
- `docs/output/output_logs/` — AI agent execution logs
- `docs/output/srs-systems/` — System-wide SRS (generated once)
- `specs/[FEATURE_NAME]/` — Spec-Kit artifacts (`spec.md`, `plan.md`, `tasks.md`, etc.)
- `apps/okr/backend/` + `apps/okr/frontend/` — Source code (NestJS backend + React frontend)
- Source code remains in the locations defined by the adopted project
---
@@ -329,11 +337,16 @@ The entire flow is orchestrated by **`casan.pipeline`**, which executes all step
│ ├── bd-ipa-template.md
│ └── dd-ipa-template.md
│
├── apps/
│ └── <project-id>/domain/ # Per-project CASAN domain pack
│ ├── input/
│ │ ├── requirement.md # Project requirements
│ │ └── architecture.md # Architecture and constraints
│ ├── corpus/ # Benign/red-team evaluation corpus
│ ├── golden-runs/ # Golden outputs for drift checks
│ └── traceability-map.json
│
├── docs/
│ ├── input/ # Input for the entire AI-SDLC flow
│ ├── change-request/ # Change requests
│ │ └── okr-requirement.md # Raw requirements from customers
│ ├── technical_architecture.md # Technical stack decisions and constraints
│ └── output/ # Output of the AI-SDLC process
│ ├── ipa-docs/ # IPA documents
│ │ ├── srs/ # SRS documents per module
@@ -344,21 +357,9 @@ The entire flow is orchestrated by **`casan.pipeline`**, which executes all step
│ ├── output_logs/ # AI agent execution logs (per feature)
│ └── srs-systems/ # System-wide SRS (generated once, pre-flow)
│
├── specs/ # Spec-Kit feature artifacts (per feature)
│ ├── 001-access-authentication/
│ ├── 002-workspace-dashboard/
│ ├── 003-objective-keyresult/
│ └── 004-review-collaboration/
│
├── e2e/ # End-to-end tests (Playwright)
│ └── auth/ # Auth E2E tests
│
├── apps/okr/frontend/ # React + Vite SPA (source code)
│
├── apps/okr/backend/ # NestJS API service (source code)
│
├── docker/ # Docker utilities
└── docker-compose.yml # Docker Compose orchestration
├── specs/ # Optional Spec-Kit feature artifacts
├── <existing-source-directories>/ # Project-owned; CASAN init preserves them
└── <existing-ci-and-tooling>/ # Project-owned; CASAN init preserves them
```
### Directory Details
@@ -381,15 +382,10 @@ Spec-Kit configuration directory. Stores project-wide principles, scripts, and a
- **`scripts/`** — Utility scripts in `bash/` and `powershell/` for automation tasks.
- **`templates/`** — Format templates used by agents to generate artifacts. Includes Spec-Kit templates (`spec-template.md`, `plan-template.md`, `tasks-template.md`, `constitution-template.md`, `checklist-template.md`, `agent-file-template.md`) and IPA templates (`srs-template.md`, `bd-ipa-template.md`, `dd-ipa-template.md`).
#### `apps/okr/domain/input/`
Input data for the AI-SDLC flow:
- **`okr-requirement.md`** — Raw requirement documents from the customer.
#### `docs/technical_architecture.md`
Technical stack decisions and constraints (languages, frameworks, infrastructure).
#### `apps/okr/domain/input/change-request/`
Change requests applied during the project lifecycle (e.g., `change-0406.md`).
#### `apps/<project-id>/domain/input/`
Per-project requirements and architecture used by CASAN gates and traceability.
The L2 initializer creates missing domain-pack files without overwriting existing
project content.
#### `docs/output/`
All artifacts produced by the AI-SDLC process:
@@ -402,115 +398,7 @@ All artifacts produced by the AI-SDLC process:
Each folder contains `spec.md`, `plan.md`, `tasks.md`, `checklists/`, and `contracts/`. This directory serves as the **knowledge base** for agents.
#### Source Code
Source code generated by Agents during `speckit.implement` (STEP 10).
`apps/okr/backend/` and `apps/okr/frontend/` contain the application source code, co-located with the monorepo root:
- **`apps/okr/backend/`** — NestJS application. `src/` contains feature modules (`auth/`, `users/`, `objectives/`, `workspaces/`, `common/`). `prisma/` contains `schema.prisma`, migrations, and `seed.ts`. `test/` contains unit tests organized by module (`auth/`, `objectives/`, `users/`). Includes `Dockerfile`.
- **`apps/okr/frontend/`** — React + Vite SPA (TypeScript). `src/pages/` for route-level components (Login, ForgotPassword, Dashboard, CreateObjective, EditObjective, ObjectiveDetail, KeyResultDetail); `src/components/` organized by feature (`auth/`, `dashboard/`, `objective-detail/`, `objective-form/`), `layout/` (Sidebar, AppHeader, AppLayout) and `ui/` (Button, Input, Alert). `src/hooks/` (useAuth, useObjectives, useUsers, useWorkspaces), `src/lib/` (api, queryClient), `src/schemas/`, `src/types/`. `test/` contains unit tests (`hooks/`, `pages/`). Includes `Dockerfile`.
- **`e2e/`** — End-to-end tests using Playwright, organized by feature (e.g., `auth/auth.spec.ts`).
- **`docker/`** — Docker utilities. Main orchestration is in `docker-compose.yml` at the project root.
---
## OKR Web Application — Quick Start
### Description
The **OKR Web Application** is a full-stack web app for managing Objectives and Key Results (OKR). It consists of:
- **Backend**: NestJS REST API with Prisma ORM and MySQL
- **Frontend**: React + Vite SPA with Tailwind CSS
- **Database**: MySQL 8.0
### Prerequisites
- [Docker](https://docs.docker.com/get-docker/) and Docker Compose
- Node.js 20.x and npm (for local development without Docker)
### Quick Start (Docker)
```bash
# Navigate to the repository root
cd <repo-root>
# Build and start all services (MySQL + Backend + Frontend)
docker compose up --build
# To run in detached mode
docker compose up --build -d
```
Docker Compose will automatically:
1. Start MySQL and wait for it to be healthy
2. Run Prisma migrations (`prisma migrate deploy`)
3. Seed the database with default data (`prisma db seed`)
4. Start the NestJS backend
5. Start the React frontend dev server
### Default Credentials
| Role | Email | Password |
|------|-------|----------|
| Admin | `admin@okr.local` | `Password@123` |
| Manager | `manager@okr.local` | `Password@123` |
| Employee | `employee@okr.local` | `Password@123` |
### Service URLs
| Service | URL |
|---------|-----|
| Frontend | http://localhost:5173 |
| Backend API | http://localhost:3000 |
| API Documentation (Swagger) | http://localhost:3000/api/docs |
| MySQL | localhost:3307 |
### Development Without Docker
#### Backend
```bash
cd apps/okr/backend
# Install dependencies
npm install
# Copy and configure environment variables
cp .env.example .env
# Edit .env — set DATABASE_URL to point to your local MySQL instance
# Run database migrations
npx prisma migrate dev
# Seed the database
npx prisma db seed
# Start the development server
npm run start:dev
```
#### Frontend
```bash
cd apps/okr/frontend
# Install dependencies
npm install
# Copy and configure environment variables
cp .env.example .env
# Edit .env if the backend runs on a different port
# Start the Vite dev server
npm run dev
```
The frontend will be available at http://localhost:5173 and proxies API requests to http://localhost:3000/api/v1.
### Stopping Services
```bash
# Stop all containers
docker compose down
# Stop and remove volumes (resets the database)
docker compose down -v
```
CASAN does not prescribe NestJS, React, Java, .NET, or any other application
stack. Existing source, CI, `.claude`, `.github`, agents, skills, prompts, and
project instructions remain project-owned. Hybrid init only adds or merges the
CASAN integration files documented above.
+30
View File
@@ -69,6 +69,9 @@ casan level set 2 # đổi level project (không cần ini
`init` nhiều lần không nhân đôi hook.
- **Guardrail:** `init` **từ chối** khi target chính là một CASAN source hub (để
không tự chặn agent đang phát triển CASAN); dùng `--force` nếu thực sự cần.
- Project cũ có vendored `packages/casan-harness` **không bị xem là source hub**:
init tự migrate sang hybrid, giữ nguyên harness/CI/scripts cũ để tương thích và
không yêu cầu `--force`.
`--level 3` (platform) chỉ áp base L2 + nhắc rằng platform là service deploy riêng;
`--level 4` (enterprise) bị từ chối (chưa ship).
@@ -100,6 +103,33 @@ Bootstrap `.casan/casan-hook.py` tự đọc `config.json`; developer không cò
source `.casan/agentic.env` trước khi mở IDE. Mỗi invocation kiểm tra live hash
của harness global so với `version.lock` trước khi chạy adapter.
### Project đã có `.claude`, `.github`, agents, skills hoặc CASAN vendored
Chỉ cần commit/backup trạng thái hiện tại, cài CASAN global rồi chạy init tại
project root, kể cả khi đường dẫn có khoảng trắng:
```bash
cd '/path/to/Basic Design (Screen&Report)_v2.7'
casan init --project basic-design-v27 --client claude,codex,vscode-copilot
casan doctor
```
Quy tắc migration:
- `.claude/agents`, `.claude/skills`, `.claude/commands`, `.github/**` và
workflow hiện hữu không bị xóa hoặc ghi đè.
- CASAN chỉ merge handler của mình vào `.claude/settings.json` và
`.codex/hooks.json`; cấu hình/hook không thuộc CASAN được giữ nguyên.
- Nếu có CASAN vendored cũ, `packages/casan-harness`, `bin/casan-chat`, CI và
evidence cũ được giữ lại. Chỉ xóa chúng sau khi CI/scripts/domain smoke đã
chuyển sang harness global.
- Block legacy nằm đúng giữa marker
`CASAN_PROMPT_ENFORCEMENT_START/END` được nâng cấp tự động.
- Prose legacy nằm ngoài marker không bị sửa âm thầm; `init` và `doctor` liệt kê
file cần review. Đặc biệt phải bỏ tuyên bố cũ rằng Claude/Codex direct chat
luôn nằm ngoài CASAN, vì Plan-20 project hooks đã thay đổi hành vi đó.
- Codex vẫn cần `/hooks` trust; Copilot built-in vẫn cần explicit `@casan`.
Sau `init`, developer gõ prompt bình thường trong client — trace H1→H7 + H6 theo
Plan-20. Repo chỉ có mấy file config nhỏ; nâng cấp harness làm ở `$CASAN_HOME`.
@@ -3,8 +3,8 @@
## Outcome
CASAN can create and govern multiple independent applications without embedding product knowledge
in root `.github`, shared harness code, patch verification, or pipeline orchestration. OKR and
Service Desk are consumers of the same versioned contract.
in root `.github`, shared harness code, patch verification, or pipeline orchestration. Demo
fixtures and Service Desk are consumers of the same versioned contract.
## Trust boundaries
@@ -70,6 +70,6 @@ update fails, the incomplete directory is removed and the project is not exposed
## Verification evidence
Automated tests cover valid manifests, traversal rejection, command allowlisting, conflicting-file
protection, idempotent scaffold/harness registration, generated NestJS/React build and tests, OKR
compatibility, Service Desk isolation, source-generation review loops, and per-project patch
protection, idempotent scaffold/harness registration, generated NestJS/React build and tests,
demo-fixture compatibility, Service Desk isolation, source-generation review loops, and per-project patch
verification command selection.
@@ -222,7 +222,7 @@ coding agent. Miễn command đi qua harness, evidence được ghi theo cùng c
## 7. Case 3 — dự án Service Desk thật đã có trong repository này
Đây là bằng chứng CASAN không chỉ dùng được cho OKR:
Đây là ví dụ một project consumer độc lập với các demo fixture:
```bash
node --test apps/service-desk/test/ticket.test.mjs
@@ -237,7 +237,8 @@ bash packages/casan-harness/scripts/bash/verify-harness-reuse.sh
```
Service Desk có source/test riêng và Domain Pack riêng. Nó dùng chung đúng một
harness version với OKR. Không có gate nào được copy riêng cho Service Desk.
harness version với các project khác. Không có gate nào được copy riêng cho
Service Desk.
## 8. Case 4 — muốn model thực sự “xây dự án cho tôi”
+6 -2
View File
@@ -1,7 +1,7 @@
# CASAN Plan-21 — Hybrid Global Install + `casan init`
> Ngày lập: 2026-07-23
> Trạng thái: **IMPLEMENTED + HARDENED — global install + selectable IDE init + pinned global bootstrap + VS Code `@casan` + merge-safe (60/60 hybrid acceptance)**
> Trạng thái: **IMPLEMENTED + HARDENED — global install + selectable IDE init + pinned global bootstrap + VS Code `@casan` + merge-safe/vendored migration (66/66 hybrid acceptance)**
> Liên quan: [Plan-20](CASAN_PLAN_20_AGENTIC_CLIENT_INTEGRATION.md) (adapter/hook là base phổ quát), [CASAN_INSTALL_HYBRID.md](../casan/CASAN_INSTALL_HYBRID.md)
## 1. Mục tiêu
@@ -42,7 +42,7 @@ harness global thao tác đúng trên state của dự án hiện tại.
| Integrity hash primitive | `packages/casan-harness/scripts/python/harness_hash.py` |
| `casan init` / `verify` | `packages/casan-devkit/casan-init.py` |
| CLI wiring | `bin/casan` (`init`, `verify-harness`) |
| Acceptance suite | `packages/casan-devkit/tests/hybrid-install-tests.sh` — **60/60 PASS** |
| Acceptance suite | `packages/casan-devkit/tests/hybrid-install-tests.sh` — **66/66 PASS** |
| Adoption doc | `docs/casan/CASAN_INSTALL_HYBRID.md` |
### Level selection (4 packaging levels) & safe adoption
@@ -65,6 +65,10 @@ harness global thao tác đúng trên state của dự án hiện tại.
`@casan` Chat Participant. Built-in Copilot không bị claim là global-intercept.
- **Source-hub guardrail:** `init` refuses to adopt a CASAN source hub into itself
(would block the developing agent); `--force` overrides.
- **Vendored migration:** a consumer repo containing an older
`packages/casan-harness` is distinguished from the product source hub,
migrated without `--force`, and retains its project-owned agents, skills,
prompts, workflows, scripts, evidence, and compatibility harness.
## 5. Definition of Done
+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":
@@ -164,13 +164,71 @@ LVL=$( ( cd "$L2" && "$DKC" level show ) | python3 -c 'import json,sys;print(jso
[ "$LVL" = "2" ] && pass "casan level show reports project target level" || fail "level show wrong ($LVL)"
echo "===== ⑨ SAFETY: init refuses to adopt a CASAN source hub into itself ====="
HUB="$WORK/fakehub"; mkdir -p "$HUB/packages/casan-harness/scripts/bash"
HUB="$WORK/fakehub"
mkdir -p "$HUB/packages/casan-harness/scripts/bash" \
"$HUB/packages/casan-devkit" "$HUB/packaging"
echo '#casan' > "$HUB/packages/casan-harness/scripts/bash/casan-harness.sh"
echo '#devkit' > "$HUB/packages/casan-devkit/casan-init.py"
echo '{}' > "$HUB/packaging/levels.json"
echo '#!/bin/sh' > "$HUB/install.sh"
GRC=0; ( cd "$HUB" && "$DKC" init --project hub >/dev/null 2>"$WORK/hub.err" ) || GRC=$?
[ "$GRC" -eq 65 ] && pass "init refuses a source hub (rc=65)" || fail "source-hub not refused (rc=$GRC)"
[ ! -f "$HUB/.claude/settings.json" ] && pass "no hooks written into the refused hub" || fail "hooks written into source hub"
( cd "$HUB" && "$DKC" init --project hub --force >/dev/null 2>&1 ) && pass "--force overrides the source-hub guard" || fail "--force did not override guard"
echo "===== ⑨b MIGRATION: existing vendored project shell is preserved ====="
LEGACY="$WORK/legacy-vendored"
mkdir -p "$LEGACY/packages/casan-harness/scripts/bash" \
"$LEGACY/.claude/commands/bd" "$LEGACY/.github/skills/domain" \
"$LEGACY/.gitea/workflows" "$LEGACY/.casan"
echo '#legacy harness' > "$LEGACY/packages/casan-harness/scripts/bash/casan-harness.sh"
echo 'existing command' > "$LEGACY/.claude/commands/bd/generate.md"
echo 'existing skill' > "$LEGACY/.github/skills/domain/SKILL.md"
echo 'existing workflow' > "$LEGACY/.gitea/workflows/existing.yml"
cat > "$LEGACY/AGENTS.md" <<'EOF'
Project-owned instructions.
<!-- CASAN_PROMPT_ENFORCEMENT_START -->
## CASAN prompt enforcement — mandatory
- The supported prompt boundary is `bin/casan-chat`.
<!-- CASAN_PROMPT_ENFORCEMENT_END -->
EOF
cat > "$LEGACY/.casan/prompt-policy.json" <<'EOF'
{"schema_version":1,"mode":"enforced","project_id":"legacy-vendored","domain_root":"apps/legacy-vendored/domain","direct_external_ui":"not_interceptable_not_certified"}
EOF
chmod 0555 "$LEGACY/.claude"
( cd "$LEGACY" && "$DKC" init --project legacy-vendored --client claude,codex >/dev/null 2>&1 ) \
&& pass "vendored application migrates without unsafe --force" \
|| fail "vendored application was mistaken for a source hub"
[ -f "$LEGACY/.claude/commands/bd/generate.md" ] \
&& [ -f "$LEGACY/.github/skills/domain/SKILL.md" ] \
&& [ -f "$LEGACY/.gitea/workflows/existing.yml" ] \
&& pass "existing commands, GitHub skills, and CI are preserved" \
|| fail "existing project shell content was changed"
grep -q 'CASAN IDE governance' "$LEGACY/AGENTS.md" \
&& grep -q 'Project-owned instructions.' "$LEGACY/AGENTS.md" \
&& pass "only the CASAN-managed legacy instruction block is migrated" \
|| fail "managed prompt-boundary migration was not precise"
python3 - "$LEGACY/.casan/prompt-policy.json" <<'PY' \
&& pass "legacy prompt policy records hybrid client-dependent routing" \
|| fail "legacy prompt policy was not migrated"
import json,sys
d=json.load(open(sys.argv[1]))
assert d["direct_external_ui"] == "client_dependent_see_config"
assert d["agentic_clients"] == ["claude","codex"]
assert d["hybrid_adoption"] is True
PY
[ -f "$LEGACY/packages/casan-harness/scripts/bash/casan-harness.sh" ] \
&& pass "vendored harness is retained for compatibility, not deleted" \
|| fail "migration deleted the existing vendored harness"
python3 - "$LEGACY/.claude" <<'PY' \
&& pass "owner-controlled read-only config directory mode is restored" \
|| fail "migration did not restore the original directory permissions"
import os,stat,sys
assert stat.S_IMODE(os.stat(sys.argv[1]).st_mode) == 0o555
PY
# Restore fixture writability so the suite's temporary-directory cleanup works.
chmod 0755 "$LEGACY/.claude"
echo "===== ⑩ VS Code/Copilot @casan packaging + install + doctor ====="
FAKE_BIN="$WORK/fake-bin"; mkdir -p "$FAKE_BIN"
cat > "$FAKE_BIN/code" <<'EOF'