diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 4906989..8ec8a18 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ { "name": "goax", "description": "AI 에이전트 결과 일관성을 환경으로 통제하는 4계층 하네스 (Triage·Constitution·Module·Spec/ADR) + Spirit·Mistake Loop. bash+markdown only, 자연어로 도입. Claude Code·OpenCode 지원.", - "version": "0.7.5", + "version": "0.7.6", "author": { "name": "bluecheat", "email": "itsinil@gmail.com" @@ -30,5 +30,5 @@ ] } ], - "version": "0.7.5" + "version": "0.7.6" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 797ab3f..184757e 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "goax", - "version": "0.7.5", + "version": "0.7.6", "description": "AX 4-Layer harness — Triage / Constitution / Module / Spec·ADR + Spirit·Mistake Loop. Bash + Markdown only, with Claude-driven onboarding. Multi-CLI: Claude Code (native) + OpenCode (Hybrid compat via AGENTS.md SSOT + opencode.json).", "author": { "name": "bluecheat", diff --git a/VERSION b/VERSION index 8bd6ba8..c006218 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.5 +0.7.6 diff --git a/agents/lane-worker.md b/agents/lane-worker.md index 929eb67..440c204 100644 --- a/agents/lane-worker.md +++ b/agents/lane-worker.md @@ -40,6 +40,14 @@ description: "실행 레인 sub-agent — tasks.md 를 파일 소유권으로 깨진 환경이에요 - **번호를 손으로 정하기** — ADR·spec 번호는 `next-spec-num.sh --reserve --slug ` 로 받아요. 동시 세션이 같은 번호를 잡은 적이 있어요 +- **공용 자원을 통째로 쓰기** — 레인은 같은 머신에서 동시에 돌아요. 파일이 아닌 자원도 남의 것이 섞여 있어요 + - 포매터·린터의 `--write`/`--fix` 는 **소유 파일 경로만** 인자로 줘요 (`biome check --write `). + 디렉토리·글롭을 주면 명령 하나로 다른 레인 소유 파일을 고쳐요 — 실제로 `src/shared` 전체 포맷이 바뀐 적이 있어요 + - 브라우저 세션은 레인 고유 이름을 붙여요 (`--session lane-<이름>`). `close --all` 이나 전역 kill 은 + 다른 레인의 세션까지 닫아요 + - 빌드 출력 경로(DerivedData · `dist` · 캐시)는 브리프의 `## 공용 자원` 이 정한 경로를 써요. 레인마다 새로 + 잡으면 캐시가 레인 수만큼 쌓여요 — 디스크를 크게 쓰는 작업 전에는 `df -h` 를 보고, 브리프의 기준보다 + 여유가 적으면 멈추고 보고해요 - **체크박스를 켜기** — `tasks.md` 는 이 레인의 소유 파일이 아니에요. `- [x]` 는 코디네이터가 보고를 받아 원장에 적고(`lanes-dispatch.sh --report`) 검증 명령을 직접 다시 돌린 뒤에 켜요. 레인이 켜면 `tasks-gate.sh` 가 "보고 없이 완료 표시" 로 잡아 spec 완료를 막아요 diff --git a/changelog/0.7.6.md b/changelog/0.7.6.md new file mode 100644 index 0000000..14fbbd6 --- /dev/null +++ b/changelog/0.7.6.md @@ -0,0 +1,38 @@ +# 0.7.6 — 무관한 커밋엔 spec 경고가 한 줄만 · 시크릿 오탐·위치 표시 · 스크래치패드 정리가 막히지 않아요 + +## 무엇이 좋아졌나 — 누구에게 체감되나 + +> spec 을 진행하면서 커밋하는 사람, TS·Kotlin·Swift 프로젝트, 레인을 여러 개 띄우는 사람에게 체감돼요. + +- **이번 커밋과 무관한 spec 경고가 매번 나오지 않아요.** 활성 spec 의 완료 게이트(`pre-commit/spec-completion-gate.sh`)는 + README 한 줄만 커밋해도 미완료 task·AC 경고를 냈어요. 이제 스테이지 파일이 그 spec 디렉토리 안이거나 tasks.md + `files:` 경로와 겹칠 때만 자세히 말하고 막아요. 아니면 `spec X 미완료 N — 이번 커밋과 무관해 건너뛰어요` 한 줄만 내고, + `sensors.mode=fail` 에서도 통과해요. +- **TS 타입 선언이 시크릿으로 잡히지 않아요.** `token: string,` · `messageFor?(…, token: string, …)` 이 CRITICAL 위반이 + 되던 것 — 이제 타입 자리는 빼고 보고, `const token: string = "…"` 같은 값 대입은 그대로 잡아요. + 따옴표 없는 YAML 값이 영문자뿐(`secret: MySuperSecret`)이거나 flow 표기(`{token: abc123, …}`)이면 놓쳐요. +- **위반 경고마다 `file:line — 규칙` 이에요.** 시크릿도 줄 번호까지(값은 안 찍어요), 요약 줄에도 앞 3곳의 위치가 실려요. +- **세션 스크래치패드의 빌드 캐시를 지울 수 있어요.** `cd <스크래치패드> && rm -rf Build …; df -h /` 가 뒤의 `df -h /` + 때문에 "재귀 rm + 루트 경로" 로 막히던 것 — 이제 rm 이 든 명령 안에서만 경로를 봐요. 루트·시스템·홈 삭제는 그대로 + 막고, `cd / && rm -rf *` · `pushd / && rm -rf *` · 줄 이어쓰기(`rm -rf \⏎ /`)도 막아요. +- **spirit 룰의 `contract:` 를 doctor 가 추적해요.** 룰 frontmatter 에 짝 테스트 경로를 적으면, 테스트 파일이 없거나 + `contract_ids:` 로 적은 SP-ID 가 테스트 본문에 없을 때 `spirit-lint` 가 알려주고, `rules-index` 는 테스트가 막는 룰에 🧪 를 붙여요. +- **레인 브리프에 공용 자원 칸이 생겼어요.** 포매터 `--write` 는 소유 파일만, 브라우저 세션은 레인 이름으로, 빌드 출력은 + 코디네이터가 정한 한 경로로 — 레인 하나가 다른 레인 파일을 포맷하거나 세션을 닫거나 디스크를 채우던 일을 막아요. +- **팀원(teammate)으로 띄운 레인은 SendMessage 로 한 번만 보고해요.** 같은 보고가 세 번 오고 idle 통지에서 잘리던 것. + +## 바뀐 것 + +- `pre-commit/spec-completion-gate.sh` — 스테이지 파일 ↔ spec 디렉토리·tasks.md `files:` (펜스 안 예시는 건너뜀) +- `common.sh` — `goax_secret_scan_file`(kv-detect 전에 타입 자리 제거, `줄번호 TAB 라벨` 출력) · `goax_rule_contracts` +- `pre-commit/critical-rule-grep.sh` · `check-mistake-secrets.sh` — `file:line — 규칙`, 요약 줄 위치 +- `pre-bash/block-destructive.sh` — 단순 명령(세그먼트) 단위 판정, 줄 이어쓰기를 먼저 붙이고 cd·pushd 뒤 상대 경로 전부를 CATASTROPHIC 으로 +- `spirit-lint.sh` F6 · `rules-index.sh` contract 열 · doctor §3.7 · 룰 템플릿 frontmatter 안내 +- `agents/lane-worker.md` · `agents/lane-scout.md` · `skills/lane` §10·§11 · `lane-mode.md` §2·§3 +- 새 프로브 예시 `templates/zero/probe/examples/secret-scan-typed.sh` + +## 검증 + +- smoke 전체 통과 (macOS bash 3.2) · CI macOS/ubuntu +- 추가: spec 완료 게이트 3건 · block-destructive 회귀(스크래치패드·heredoc·세그먼트·cd/pushd·줄 이어쓰기) · + 시크릿 타입 선언/값 대입 · 위반 위치 출력 · spirit-lint F6 · rules-index contract diff --git a/changelog/README.md b/changelog/README.md index 5e6f319..66d1fdd 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -4,6 +4,7 @@ ## 버전 +- [0.7.6](0.7.6.md) — 무관한 커밋엔 spec 경고가 한 줄만 · TS 타입 선언 시크릿 오탐 · 위반마다 file:line · 스크래치패드 정리가 안 막혀요 · `contract:` 추적 · 레인 공용 자원 - [0.7.5](0.7.5.md) — 인계 노트에 spec ID 만 적어도 Stop 게이트가 알아봐요 - [0.7.4](0.7.4.md) — 레인의 일부만 맡겨도 원장이 그대로 적어요 (`--dispatch T010,T011` · `mark-task --task T010,T011`) - [0.7.3](0.7.3.md) — audit 이 경고만 있는 룰을 만들던 것: 룰 본문은 가이드("어떻게 해야 하나")부터, grep 경고는 커밋을 멈춰야 하는 룰에만 · "집행 사다리" → "막는 방법 우선순위" diff --git a/skills/doctor/SKILL.md b/skills/doctor/SKILL.md index 50f89e1..2490fee 100644 --- a/skills/doctor/SKILL.md +++ b/skills/doctor/SKILL.md @@ -49,7 +49,7 @@ Spirit 카테고리 수를 `spirit-lint.sh` 로, mistake 카테고리 집계를 |---|---|---| | **Layer 1** | Constitution 존재 + 시그널 라벨 + 4계층 인덱스 표 | `rules-index.sh` (§3.13) · `doctor-scan.sh reach` (§3.8) | | **Layer 0 / 설정** | `.ax/config.yml` (domain_risk 5+ 권장) + `.ax/version` | `zero-domain-risk.sh --show` | -| **Cross-cut Spirit** | values/tone placeholder · rules/ 1개+ · 헤더 형식 · 토큰 중복 | `spirit-lint.sh` (§3.7) | +| **Cross-cut Spirit** | values/tone placeholder · rules/ 1개+ · 헤더 형식 · 토큰 중복 · `contract:` 짝 테스트 | `spirit-lint.sh` (§3.7) | | **Cross-cut Mistake Loop** | `.ax/mistakes/` 존재 | — | | **Layer 3** | `.ax/docs/adr/` 1개+, `_templates/spec/` (drift 는 §3.5) | — | | **Layer 2** | `.ax/modules/*/rules.md` 카운트 + 도달 | `doctor-scan.sh reach` | @@ -192,6 +192,7 @@ SL_BAD=$(echo "$SL" | jq -r '.result.bad_headers[] | "\(.file):\(.line) — \(.t SL_DUP=$(echo "$SL" | jq -r '.result.duplicates[] | "\(.token) — \(.files | join(", "))"') SL_MISS=$(echo "$SL" | jq -r '.result.missing_files + .result.missing_frontmatter | join(", ")') SL_PH=$(echo "$SL" | jq -r '.result.placeholders[] | "\(.file):\(.line)"') +SL_CON=$(echo "$SL" | jq -r '.result.contracts[] | "\(.file) — 없는 테스트 \(.missing | join(", ")) · 계약 없는 룰 \(.uncovered | join(", "))"') ``` 헤더 형식 `## SP--: 제목` 의 SSOT 는 `.ax/docs/reference/rules-tokens.md`. 0건이면 본 표 `◆ Cross-cut — Spirit` 줄의 ✅ 만: @@ -201,6 +202,7 @@ SL_PH=$(echo "$SL" | jq -r '.result.placeholders[] | "\(.file):\(.line)"') ❗ 비표준 헤더 — ❌ 중복 토큰 — ❗ placeholder 잔재 — + ❗ 계약(contract:) — ``` 다음 단계 `[n] ✅ spirit lint 정리 — 비표준 헤더 수정 + 중복 토큰 해소 (사용자와 함께, 자동 수정 X)`. "spirit 점검" 으로 불렸으면 이 절만 보고하고 끝내요. `--strict` 면 placeholder 도 fail. diff --git a/skills/lane/SKILL.md b/skills/lane/SKILL.md index dd7282f..9c4839f 100644 --- a/skills/lane/SKILL.md +++ b/skills/lane/SKILL.md @@ -247,6 +247,11 @@ bash .ax/scripts/bash/tasks-plan.sh --spec "$SPEC" --json ## 금지 파일 <다른 레인 소유 · 공용 설정 · 버전 파일> +## 공용 자원 — 비우지 마세요 (해당 없음이면 "없음") +- 포매터·린터 쓰기: 소유 파일 경로만 인자로 (디렉토리·글롭 금지) +- 브라우저 세션 이름: — `close --all`·전역 kill 금지 +- 빌드 출력 경로: <코디네이터가 정한 한 경로, 예: /DerivedData> · 디스크 여유 기준: <예: 10GB> + ## 규율 - **커밋하지 않아요.** 워킹 트리에 변경만 남기세요. `git add -A` 는 특히 금지 — 동시 세션의 파일까지 쓸어담아요. diff --git a/skills/spec-implement/references/lane-mode.md b/skills/spec-implement/references/lane-mode.md index ea31b4e..ab160a1 100644 --- a/skills/spec-implement/references/lane-mode.md +++ b/skills/spec-implement/references/lane-mode.md @@ -44,7 +44,7 @@ bash .ax/scripts/bash/lanes-dispatch.sh --spec "$SPEC" --dispatch A --json `ready`·`dispatched_unreported` 를 다시 봐요. 진짜 실패(파일 소유 충돌)는 메시지가 달라요. 브리프는 `lane` skill §10 형식 그대로예요 — 소유 파일(그 레인 task 들의 `files:` 합집합) · -금지 파일(다른 레인 소유 + `protected_paths` + 버전 파일) · task ID 와 각 `검증:` 명령 · +금지 파일(다른 레인 소유 + `protected_paths` + 버전 파일) · 공용 자원(포매터 범위 · 브라우저 세션 이름 · 빌드 출력 경로 · 디스크 기준 — 비어 있으면 띄우기 전에 채워요) · task ID 와 각 `검증:` 명령 · 정지 조건 · "커밋하지 않는다" · "체크박스는 켜지 않는다" · 보고 경로. 보고 경로는 레인을 띄우는 방식이 정해요: | 띄우는 방식 | 레인의 도구 | 보고 경로 | 레인의 최종 응답 | diff --git a/templates/default/.ax/_templates/spirit/rule.md b/templates/default/.ax/_templates/spirit/rule.md index a75f5cc..4ab750f 100644 --- a/templates/default/.ax/_templates/spirit/rule.md +++ b/templates/default/.ax/_templates/spirit/rule.md @@ -14,6 +14,9 @@ enforced_by: # 무엇이 이 룰을 실제로 막나 — # hook:<경로> · external:<도구> · human:<게이트> · TODO: enforced_kind: human # block | warn | grep | arch | human | missing # grep = 아래 룰의 `검출 패턴:` 마커가 집행 (패턴 없는 룰은 doctor I7) +# contract: <테스트 경로> # 이 파일의 룰을 막는 짝 테스트 (선택, 목록도 돼요). 테스트 이름에 +# # SP-ID 를 넣으면(`it('SP-X-001: …')`) doctor 가 어느 룰이 막혀 있나 봐요 +# contract_ids: [SP-X-001] # 계약 대상만 고를 때 (없으면 파일의 모든 SP-ID) # adr: .ax/docs/adr/-*.md # 결정 근거 ADR (선택) --- diff --git a/templates/default/.ax/hooks/README.md b/templates/default/.ax/hooks/README.md index 8b811e8..1a1db7b 100644 --- a/templates/default/.ax/hooks/README.md +++ b/templates/default/.ax/hooks/README.md @@ -10,7 +10,7 @@ AI 에이전트의 결과를 *작업 후* 자동 검증하는 sensor 4종(Comput | pre-bash/ | bash 도구 호출 직전 | 파괴적 명령 차단 · git 훅 우회(`--no-verify` 등) 차단 · 프로젝트 안 되돌리기 어려운 명령은 사실 확인 · `git commit` 감지 시 pre-commit 체인 위임 | | pre-edit/ | Edit/Write 직전 | 보호 경로 변경 확인 + spirit 룰 점검 · 룰 경로 주입 · 이 파일에 걸린 룰을 안 읽었으면 편집 차단(`rule-read-gate`) · 품질 설정 수정은 이유 먼저(`quality-config-gate`) | | post-edit/ | Edit/Write 직후 | `commands.lint_file` 로 편집한 파일 하나를 검사, 실패하면 출력을 모델에게 (막지 않음 · 비어 있으면 아무것도 안 함) | -| pre-commit/ | git commit 직전 | CRITICAL 룰 정적 검출 (위반 시 차단/경고만 — 자동 캡처는 폐기, §"Mistake 캡처" 참고) | +| pre-commit/ | git commit 직전 | CRITICAL 룰 정적 검출 (위반 시 차단/경고만 — 자동 캡처는 폐기, §"Mistake 캡처" 참고) · 활성 spec 완료 게이트 (스테이지 파일이 그 spec 디렉토리나 tasks.md `files:` 에 걸릴 때만 자세히 · 아니면 한 줄) | | subagent-start/ | 서브에이전트가 뜨는 순간 | Constitution·Spirit·현재 spec·인계 노트 **경로**를 additionalContext 로 — 하네스가 메인 세션 밖으로 닿게 (goax 자기 에이전트는 제외) | | stop/ | 턴이 끝나려는 순간 | 활성 spec(implementing·review)이 완료 게이트 미통과면 **한 번** 멈춰 세우고 "마저 하기 · 보류 표기 · 인계 노트" 셋 중 하나를 시켜요 | diff --git a/templates/default/.ax/hooks/pre-commit/spec-completion-gate.sh b/templates/default/.ax/hooks/pre-commit/spec-completion-gate.sh index 177e31a..9c91f4d 100755 --- a/templates/default/.ax/hooks/pre-commit/spec-completion-gate.sh +++ b/templates/default/.ax/hooks/pre-commit/spec-completion-gate.sh @@ -12,6 +12,8 @@ # 게이트가 그냥 성가신 게 되고, 결국 우회 대상이 돼요. # # 진행 중인 spec 이 없으면(phase=idle) 조용히 통과. 커밋마다 떠들지 않아요. +# 스테이지 파일이 그 spec 에 안 걸리면(spec 디렉토리 밖 · tasks.md `files:` 와 무관) 한 줄만 내고 통과해요 — +# mode=fail 에서도요. 막는 건 그 spec 을 건드리는 커밋이에요. set -uo pipefail [ -d "${CLAUDE_PROJECT_DIR:-$(pwd)}/.ax/hooks" ] || exit 0 @@ -61,6 +63,38 @@ VIOL=$(printf '%s' "$OUT" | jq -r '.result.violations // 0') [ "${VIOL:-0}" -eq 0 ] && exit 0 +# 이번 커밋이 그 spec 에 걸릴 때만 자세히 말해요. 무관한 커밋(문서 한 줄)마다 같은 경고가 나오면 +# 아무도 읽지 않게 되고, 정작 그 spec 을 커밋할 때의 경고도 묻혀요 (실측: 커밋마다 반복). +# 걸린다 = 스테이지 파일이 spec 디렉토리 안이거나, tasks.md 의 `files:` 경로(파일 또는 디렉토리)와 겹쳐요. +SPEC_DIR_REL=".ax/docs/spec/$SPEC" +STAGED=$(git -C "$PROJECT_ROOT" -c core.quotePath=false diff --cached --name-only 2>/dev/null || true) +if [ -n "$SPEC" ] && [ -n "$STAGED" ]; then + TASK_PATHS="" + if [ -f "$PROJECT_ROOT/$SPEC_DIR_REL/tasks.md" ]; then + # 펜스 안은 형식 설명이에요 — 템플릿 예시 `path/a.kt` 를 실제 경로로 세지 않아요 + TASK_PATHS=$(awk ' + /^[[:space:]]*```/ { fence = !fence; next } + fence { next } + /^- \[[ x~X]\] / && match($0, /files:[ ]*/) { + n = split(substr($0, RSTART + RLENGTH), a, ",") + for (i = 1; i <= n; i++) { + p = a[i]; gsub(/^[ `]+|[ `]+$/, "", p); sub(/^\.\//, "", p); gsub(/\/+/, "/", p); sub(/\/$/, "", p) + if (p != "") print p + } + }' "$PROJECT_ROOT/$SPEC_DIR_REL/tasks.md") + fi + # 경로 목록은 ENVIRON 으로 넘겨요 — 줄바꿈이 든 값을 -v 로 주면 mawk 가 거부해요 + RELATED=$(printf '%s\n' "$STAGED" | GOAX_TP="$TASK_PATHS" awk -v sd="$SPEC_DIR_REL" ' + BEGIN { n = split(ENVIRON["GOAX_TP"], P, "\n") } + { s = $0 + if (index(s, sd "/") == 1) { print s; exit } + for (i = 1; i <= n; i++) if (P[i] != "" && (s == P[i] || index(s, P[i] "/") == 1)) { print s; exit } }') + if [ -z "$RELATED" ]; then + printf '\033[33m[goax gate]\033[0m spec %s 미완료 %s — 이번 커밋과 무관해 건너뛰어요\n' "$SPEC" "$OPEN" >&2 + exit 0 + fi +fi + printf '\033[33m[goax gate]\033[0m spec %s — %s/%s 완료' "$SPEC" "$DONE" "$TOTAL" >&2 [ "${PAUSED:-0}" -gt 0 ] && printf ' (보류 %s)' "$PAUSED" >&2 printf '\n' >&2 diff --git a/templates/default/.ax/scripts/bash/common.sh b/templates/default/.ax/scripts/bash/common.sh index 16c9777..7f210ec 100755 --- a/templates/default/.ax/scripts/bash/common.sh +++ b/templates/default/.ax/scripts/bash/common.sh @@ -1168,6 +1168,31 @@ goax_frontmatter_scalar() { ' "$file" } +# goax_rule_contracts +# 룰 파일 frontmatter 의 `contract:`(그 룰을 막는 짝 테스트 경로)와 `contract_ids:`(계약 대상 SP-ID)를 +# `path값` · `id값` 줄로 내요. 스칼라(`contract: a.test.ts # 주석`) · 인라인 목록(`[a, b]`) · +# 블록 목록(`- a`) 셋 다 받아요. frontmatter 밖은 안 봐요 — 본문 예시가 계약으로 읽히면 안 돼요. +goax_rule_contracts() { + local file="${1:-}" + [ -f "$file" ] || return 0 + awk ' + function clean(v) { sub(/(^|[[:space:]])#.*$/, "", v); gsub(/^[[:space:]\042\047`]+|[[:space:]\042\047`]+$/, "", v); return v } + function emit(k, v, n, i, a) { + v = clean(v) + if (v ~ /^\[/) { sub(/^\[/, "", v); sub(/\].*$/, "", v); n = split(v, a, ",") + for (i = 1; i <= n; i++) { a[i] = clean(a[i]); if (a[i] != "") print k "\t" a[i] } } + else if (v != "") print k "\t" v + } + NR == 1 && !/^---[[:space:]]*$/ { exit } + NR == 1 { next } + /^---[[:space:]]*$/ { exit } + /^contract:/ { cur = "path"; v = $0; sub(/^contract:/, "", v); emit(cur, v); next } + /^contract_ids:/ { cur = "id"; v = $0; sub(/^contract_ids:/, "", v); emit(cur, v); next } + cur != "" && /^[[:space:]]+-[[:space:]]+/ { v = $0; sub(/^[[:space:]]+-[[:space:]]+/, "", v); emit(cur, v); next } + /^[^[:space:]]/ { cur = "" } + ' "$file" +} + # goax_glob_owners # stdin `