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)
|
- [CASAN Project Shell production](docs/guides/CASAN_PROJECT_SHELL_PRODUCTION.md)
|
||||||
- [Requirements](#requirements)
|
- [Requirements](#requirements)
|
||||||
- [Input for Flow](#input-for-flow)
|
- [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)
|
- [Full AI-SDLC Flow](#full-ai-sdlc-flow)
|
||||||
- [Flow Steps](#flow-steps)
|
- [Flow Steps](#flow-steps)
|
||||||
- [Flow Diagram](#flow-diagram)
|
- [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)
|
- [AI-SDLC Usage Guide](#ai-sdlc-usage-guide)
|
||||||
- [Directory Structure](#directory-structure)
|
- [Directory Structure](#directory-structure)
|
||||||
- [Directory Details](#directory-details)
|
- [Directory Details](#directory-details)
|
||||||
- [OKR Web Application — Quick Start](#okr-web-application--quick-start)
|
|
||||||
|
|
||||||
## CASAN 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,
|
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.
|
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
|
### 3. Hành vi chat theo client
|
||||||
|
|
||||||
| Client đã enable | Prompt đi qua CASAN | Onboarding bắt buộc |
|
| 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
|
## Input for Flow
|
||||||
|
|
||||||
- `apps/okr/domain/input/okr-requirement.md` — Raw requirements from customers
|
- `apps/<project-id>/domain/input/requirement.md` — project requirements
|
||||||
- `docs/technical_architecture.md` — Technical stack decisions and constraints
|
- `apps/<project-id>/domain/input/architecture.md` — architecture and constraints
|
||||||
- `apps/okr/domain/input/change-request/` — Change requests (e.g., `change-0406.md`)
|
- Project-owned input, reference, agent, and skill directories remain untouched
|
||||||
|
|
||||||
## Output for Flow
|
## 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 |
|
| **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) |
|
| **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) |
|
| **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
|
## AI-SDLC Usage Guide
|
||||||
|
|
||||||
1. **Adopt governance**: Run `casan init`, select the IDE clients, complete any trust/VSIX onboarding, then run `casan doctor`
|
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
|
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/`
|
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
|
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/output_logs/` — AI agent execution logs
|
||||||
- `docs/output/srs-systems/` — System-wide SRS (generated once)
|
- `docs/output/srs-systems/` — System-wide SRS (generated once)
|
||||||
- `specs/[FEATURE_NAME]/` — Spec-Kit artifacts (`spec.md`, `plan.md`, `tasks.md`, etc.)
|
- `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
|
│ ├── bd-ipa-template.md
|
||||||
│ └── dd-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/
|
├── 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
|
│ └── output/ # Output of the AI-SDLC process
|
||||||
│ ├── ipa-docs/ # IPA documents
|
│ ├── ipa-docs/ # IPA documents
|
||||||
│ │ ├── srs/ # SRS documents per module
|
│ │ ├── 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)
|
│ ├── output_logs/ # AI agent execution logs (per feature)
|
||||||
│ └── srs-systems/ # System-wide SRS (generated once, pre-flow)
|
│ └── srs-systems/ # System-wide SRS (generated once, pre-flow)
|
||||||
│
|
│
|
||||||
├── specs/ # Spec-Kit feature artifacts (per feature)
|
├── specs/ # Optional Spec-Kit feature artifacts
|
||||||
│ ├── 001-access-authentication/
|
├── <existing-source-directories>/ # Project-owned; CASAN init preserves them
|
||||||
│ ├── 002-workspace-dashboard/
|
└── <existing-ci-and-tooling>/ # Project-owned; CASAN init preserves them
|
||||||
│ ├── 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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Directory Details
|
### 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.
|
- **`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`).
|
- **`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/`
|
#### `apps/<project-id>/domain/input/`
|
||||||
Input data for the AI-SDLC flow:
|
Per-project requirements and architecture used by CASAN gates and traceability.
|
||||||
- **`okr-requirement.md`** — Raw requirement documents from the customer.
|
The L2 initializer creates missing domain-pack files without overwriting existing
|
||||||
|
project content.
|
||||||
#### `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`).
|
|
||||||
|
|
||||||
#### `docs/output/`
|
#### `docs/output/`
|
||||||
All artifacts produced by the AI-SDLC process:
|
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.
|
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
|
||||||
Source code generated by Agents during `speckit.implement` (STEP 10).
|
CASAN does not prescribe NestJS, React, Java, .NET, or any other application
|
||||||
`apps/okr/backend/` and `apps/okr/frontend/` contain the application source code, co-located with the monorepo root:
|
stack. Existing source, CI, `.claude`, `.github`, agents, skills, prompts, and
|
||||||
- **`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`.
|
project instructions remain project-owned. Hybrid init only adds or merges the
|
||||||
- **`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`.
|
CASAN integration files documented above.
|
||||||
- **`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
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -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.
|
`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 (để
|
- **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.
|
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 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).
|
`--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
|
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.
|
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
|
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`.
|
Plan-20. Repo chỉ có mấy file config nhỏ; nâng cấp harness làm ở `$CASAN_HOME`.
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
## Outcome
|
## Outcome
|
||||||
|
|
||||||
CASAN can create and govern multiple independent applications without embedding product knowledge
|
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
|
in root `.github`, shared harness code, patch verification, or pipeline orchestration. Demo
|
||||||
Service Desk are consumers of the same versioned contract.
|
fixtures and Service Desk are consumers of the same versioned contract.
|
||||||
|
|
||||||
## Trust boundaries
|
## Trust boundaries
|
||||||
|
|
||||||
@@ -70,6 +70,6 @@ update fails, the incomplete directory is removed and the project is not exposed
|
|||||||
## Verification evidence
|
## Verification evidence
|
||||||
|
|
||||||
Automated tests cover valid manifests, traversal rejection, command allowlisting, conflicting-file
|
Automated tests cover valid manifests, traversal rejection, command allowlisting, conflicting-file
|
||||||
protection, idempotent scaffold/harness registration, generated NestJS/React build and tests, OKR
|
protection, idempotent scaffold/harness registration, generated NestJS/React build and tests,
|
||||||
compatibility, Service Desk isolation, source-generation review loops, and per-project patch
|
demo-fixture compatibility, Service Desk isolation, source-generation review loops, and per-project patch
|
||||||
verification command selection.
|
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
|
## 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
|
```bash
|
||||||
node --test apps/service-desk/test/ticket.test.mjs
|
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
|
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”
|
## 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`
|
# CASAN Plan-21 — Hybrid Global Install + `casan init`
|
||||||
|
|
||||||
> Ngày lập: 2026-07-23
|
> 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)
|
> 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
|
## 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` |
|
| Integrity hash primitive | `packages/casan-harness/scripts/python/harness_hash.py` |
|
||||||
| `casan init` / `verify` | `packages/casan-devkit/casan-init.py` |
|
| `casan init` / `verify` | `packages/casan-devkit/casan-init.py` |
|
||||||
| CLI wiring | `bin/casan` (`init`, `verify-harness`) |
|
| 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` |
|
| Adoption doc | `docs/casan/CASAN_INSTALL_HYBRID.md` |
|
||||||
|
|
||||||
### Level selection (4 packaging levels) & safe adoption
|
### 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.
|
`@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
|
- **Source-hub guardrail:** `init` refuses to adopt a CASAN source hub into itself
|
||||||
(would block the developing agent); `--force` overrides.
|
(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
|
## 5. Definition of Done
|
||||||
|
|
||||||
|
|||||||
@@ -26,10 +26,12 @@ launcher) or --harness.
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
from contextlib import contextmanager
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
@@ -52,6 +54,48 @@ VSCODE_EXTENSION_IDS = {
|
|||||||
"fpt-casan.casan-governed-chat",
|
"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():
|
def now_iso():
|
||||||
@@ -65,11 +109,12 @@ def devkit_root():
|
|||||||
def _copy_if_absent(src, dst, created_rel, target, created):
|
def _copy_if_absent(src, dst, created_rel, target, created):
|
||||||
if not os.path.exists(src) or os.path.exists(dst):
|
if not os.path.exists(src) or os.path.exists(dst):
|
||||||
return False
|
return False
|
||||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
|
||||||
with open(src, "rb") as fh:
|
with open(src, "rb") as fh:
|
||||||
data = fh.read()
|
data = fh.read()
|
||||||
with open(dst, "wb") as fh:
|
with owner_writable(dst):
|
||||||
fh.write(data)
|
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))
|
created.append(os.path.relpath(dst, target))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -148,17 +193,18 @@ def compute_harness_hash(harness):
|
|||||||
|
|
||||||
|
|
||||||
def _write(path, text, backups):
|
def _write(path, text, backups):
|
||||||
if os.path.exists(path):
|
with owner_writable(path):
|
||||||
bak = path + ".casan-bak"
|
if os.path.exists(path):
|
||||||
if not os.path.exists(bak):
|
bak = path + ".casan-bak"
|
||||||
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
if not os.path.exists(bak):
|
||||||
old = fh.read()
|
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
||||||
with open(bak, "w", encoding="utf-8") as fh:
|
old = fh.read()
|
||||||
fh.write(old)
|
with open(bak, "w", encoding="utf-8") as fh:
|
||||||
backups.append(bak)
|
fh.write(old)
|
||||||
os.makedirs(os.path.dirname(path), exist_ok=True)
|
backups.append(bak)
|
||||||
with open(path, "w", encoding="utf-8") as fh:
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
||||||
fh.write(text)
|
with open(path, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(text)
|
||||||
|
|
||||||
|
|
||||||
def _copy_template(src, dst, backups):
|
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):
|
if os.path.exists(path) and not os.path.exists(bak):
|
||||||
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
||||||
old = fh.read()
|
old = fh.read()
|
||||||
with open(bak, "w", encoding="utf-8") as fh:
|
with owner_writable(bak):
|
||||||
fh.write(old)
|
with open(bak, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(old)
|
||||||
backups.append(bak)
|
backups.append(bak)
|
||||||
|
|
||||||
|
|
||||||
@@ -271,10 +318,11 @@ def merge_json_hooks(target_file, template_file, marker, backups):
|
|||||||
doc[k] = v
|
doc[k] = v
|
||||||
changed = (not existed) or before != json.dumps(doc, ensure_ascii=False, sort_keys=True)
|
changed = (not existed) or before != json.dumps(doc, ensure_ascii=False, sort_keys=True)
|
||||||
if changed:
|
if changed:
|
||||||
_backup_once(target_file, backups)
|
with owner_writable(target_file):
|
||||||
os.makedirs(os.path.dirname(target_file), exist_ok=True)
|
_backup_once(target_file, backups)
|
||||||
with open(target_file, "w", encoding="utf-8") as fh:
|
os.makedirs(os.path.dirname(target_file), exist_ok=True)
|
||||||
fh.write(json.dumps(doc, ensure_ascii=False, indent=2) + "\n")
|
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"
|
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)
|
doc = _load_json_or(target_file, None)
|
||||||
if not isinstance(doc, dict) or not _remove_marker_hooks(doc, marker):
|
if not isinstance(doc, dict) or not _remove_marker_hooks(doc, marker):
|
||||||
return "unchanged"
|
return "unchanged"
|
||||||
_backup_once(target_file, backups)
|
with owner_writable(target_file):
|
||||||
with open(target_file, "w", encoding="utf-8") as fh:
|
_backup_once(target_file, backups)
|
||||||
fh.write(json.dumps(doc, ensure_ascii=False, indent=2) + "\n")
|
with open(target_file, "w", encoding="utf-8") as fh:
|
||||||
|
fh.write(json.dumps(doc, ensure_ascii=False, indent=2) + "\n")
|
||||||
return "removed"
|
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*$)",
|
r"(?ms)\n?\[hooks\]\nenabled = true\nproject_hooks = true\n(?=\s*$)",
|
||||||
"\n", cleaned)
|
"\n", cleaned)
|
||||||
cleaned = cleaned.strip() + ("\n" if cleaned.strip() else "")
|
cleaned = cleaned.strip() + ("\n" if cleaned.strip() else "")
|
||||||
_backup_once(target_file, backups)
|
with owner_writable(target_file):
|
||||||
if cleaned:
|
_backup_once(target_file, backups)
|
||||||
with open(target_file, "w", encoding="utf-8") as fh:
|
if cleaned:
|
||||||
fh.write(cleaned)
|
with open(target_file, "w", encoding="utf-8") as fh:
|
||||||
else:
|
fh.write(cleaned)
|
||||||
os.unlink(target_file)
|
else:
|
||||||
|
os.unlink(target_file)
|
||||||
return "cleaned"
|
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):
|
def select_clients(values, interactive):
|
||||||
"""Normalize repeatable/comma-separated selections.
|
"""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(
|
changed = (not existed and bool(doc)) or before != json.dumps(
|
||||||
doc, ensure_ascii=False, sort_keys=True)
|
doc, ensure_ascii=False, sort_keys=True)
|
||||||
if changed:
|
if changed:
|
||||||
_backup_once(target_file, backups)
|
with owner_writable(target_file):
|
||||||
os.makedirs(os.path.dirname(target_file), exist_ok=True)
|
_backup_once(target_file, backups)
|
||||||
with open(target_file, "w", encoding="utf-8") as fh:
|
os.makedirs(os.path.dirname(target_file), exist_ok=True)
|
||||||
fh.write(json.dumps(doc, ensure_ascii=False, indent=2) + "\n")
|
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"
|
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)
|
sys.stderr.write("casan init: target is not a directory: %s\n" % target)
|
||||||
return 66
|
return 66
|
||||||
|
|
||||||
# Guardrail: refuse to adopt a CASAN SOURCE HUB into itself. Installing the
|
# Guardrail: refuse to adopt the CASAN PRODUCT SOURCE HUB into itself.
|
||||||
# Plan-20 PreToolUse hook into the CASAN repo would block the very agent
|
# A vendored application from the previous DevKit model also contains
|
||||||
# developing CASAN (no admission => deny). Detect the hub by the harness
|
# packages/casan-harness, so that directory by itself is not sufficient to
|
||||||
# source living inside the target. Override with --force for the rare
|
# identify a source hub. Such applications are migrated in place below.
|
||||||
# intentional case.
|
|
||||||
hub_marker = os.path.join(target, "packages", "casan-harness", "scripts", "bash", "casan-harness.sh")
|
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(
|
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). "
|
"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
|
return 65
|
||||||
|
|
||||||
project = args.project or re.sub(r"[^a-z0-9-]", "-", os.path.basename(target).lower()).strip("-")
|
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/ state root marker ──
|
||||||
specify = os.path.join(target, ".specify")
|
specify = os.path.join(target, ".specify")
|
||||||
os.makedirs(os.path.join(specify, "state"), exist_ok=True)
|
with owner_writable(os.path.join(specify, "state")):
|
||||||
os.makedirs(os.path.join(specify, "logs"), exist_ok=True)
|
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")
|
gi = os.path.join(specify, ".gitignore")
|
||||||
if not os.path.exists(gi):
|
if not os.path.exists(gi):
|
||||||
_write(gi, "# CASAN runtime state — do not commit\nlogs/\nstate/\n", backups); created_add(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"}
|
{"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.
|
# ── Level 2 (devkit) adoption extras: CI workflow + domain-pack scaffold.
|
||||||
# These are the real difference between L1 (gate/hooks only) and L2 (full
|
# 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.
|
# adoption). Copied only if absent — never clobber the project's own files.
|
||||||
@@ -665,10 +842,16 @@ def cmd_init(args):
|
|||||||
"created": created,
|
"created": created,
|
||||||
"hook_merges": merges,
|
"hook_merges": merges,
|
||||||
"vscode_extension": vscode_install,
|
"vscode_extension": vscode_install,
|
||||||
|
"legacy_migration": legacy_migration,
|
||||||
"level_extras": level_extras,
|
"level_extras": level_extras,
|
||||||
"note": ("harness NOT copied into repo (hybrid model); selected client hooks "
|
"note": ("harness NOT copied into repo (hybrid model); selected client hooks "
|
||||||
"MERGED and unselected CASAN hooks removed; run `casan doctor`"),
|
"MERGED and unselected CASAN hooks removed; run `casan doctor`"),
|
||||||
}, ensure_ascii=False, indent=2))
|
}, 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:
|
if preview:
|
||||||
sys.stderr.write("casan init: NOTE — Level 3 (platform) is a PREVIEW SERVICE (Control "
|
sys.stderr.write("casan init: NOTE — Level 3 (platform) is a PREVIEW SERVICE (Control "
|
||||||
"Panel/Dashboard), adopted by DEPLOYING it, not by repo config. "
|
"Panel/Dashboard), adopted by DEPLOYING it, not by repo config. "
|
||||||
@@ -825,6 +1008,16 @@ def cmd_doctor(args):
|
|||||||
"warnings": [],
|
"warnings": [],
|
||||||
}
|
}
|
||||||
ready = integrity_ok and bootstrap_ok
|
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")
|
code = shutil.which("code")
|
||||||
installed_extensions = set()
|
installed_extensions = set()
|
||||||
@@ -888,6 +1081,15 @@ def cmd_doctor(args):
|
|||||||
checks["warnings"].append(
|
checks["warnings"].append(
|
||||||
"Only prompts explicitly sent to @casan use the CASAN-owned Copilot route; "
|
"Only prompts explicitly sent to @casan use the CASAN-owned Copilot route; "
|
||||||
"built-in Copilot chat is not globally intercepted.")
|
"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"
|
checks["status"] = "ready" if ready else "not_ready"
|
||||||
print(json.dumps(checks, ensure_ascii=False, indent=2))
|
print(json.dumps(checks, ensure_ascii=False, indent=2))
|
||||||
return 0 if ready else 2
|
return 0 if ready else 2
|
||||||
@@ -938,7 +1140,13 @@ def main(argv=None):
|
|||||||
|
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
if args.cmd == "init":
|
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":
|
if args.cmd == "verify":
|
||||||
return cmd_verify(args)
|
return cmd_verify(args)
|
||||||
if args.cmd == "level":
|
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)"
|
[ "$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 ====="
|
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 '#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=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)"
|
[ "$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"
|
[ ! -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"
|
( 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 ====="
|
echo "===== ⑩ VS Code/Copilot @casan packaging + install + doctor ====="
|
||||||
FAKE_BIN="$WORK/fake-bin"; mkdir -p "$FAKE_BIN"
|
FAKE_BIN="$WORK/fake-bin"; mkdir -p "$FAKE_BIN"
|
||||||
cat > "$FAKE_BIN/code" <<'EOF'
|
cat > "$FAKE_BIN/code" <<'EOF'
|
||||||
|
|||||||
Reference in New Issue
Block a user