optimize docs
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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”
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,9 +109,10 @@ 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 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))
|
||||
@@ -148,6 +193,7 @@ def compute_harness_hash(harness):
|
||||
|
||||
|
||||
def _write(path, text, backups):
|
||||
with owner_writable(path):
|
||||
if os.path.exists(path):
|
||||
bak = path + ".casan-bak"
|
||||
if not os.path.exists(bak):
|
||||
@@ -183,6 +229,7 @@ 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 owner_writable(bak):
|
||||
with open(bak, "w", encoding="utf-8") as fh:
|
||||
fh.write(old)
|
||||
backups.append(bak)
|
||||
@@ -271,6 +318,7 @@ 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:
|
||||
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:
|
||||
@@ -285,6 +333,7 @@ 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"
|
||||
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")
|
||||
@@ -326,6 +375,7 @@ 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 "")
|
||||
with owner_writable(target_file):
|
||||
_backup_once(target_file, backups)
|
||||
if cleaned:
|
||||
with open(target_file, "w", encoding="utf-8") as fh:
|
||||
@@ -335,6 +385,123 @@ def clean_legacy_codex_config(target_file, backups):
|
||||
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,6 +570,7 @@ 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:
|
||||
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:
|
||||
@@ -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,7 +740,9 @@ def cmd_init(args):
|
||||
|
||||
# ── .specify/ state root marker ──
|
||||
specify = os.path.join(target, ".specify")
|
||||
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):
|
||||
@@ -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":
|
||||
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'
|
||||
|
||||
Reference in New Issue
Block a user