Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -30,5 +30,5 @@
]
}
],
"version": "0.7.5"
"version": "0.7.6"
}
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.7.5
0.7.6
8 changes: 8 additions & 0 deletions agents/lane-worker.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ description: "실행 레인 sub-agent — tasks.md 를 파일 소유권으로
깨진 환경이에요
- **번호를 손으로 정하기** — ADR·spec 번호는 `next-spec-num.sh --reserve --slug <slug>` 로 받아요.
동시 세션이 같은 번호를 잡은 적이 있어요
- **공용 자원을 통째로 쓰기** — 레인은 같은 머신에서 동시에 돌아요. 파일이 아닌 자원도 남의 것이 섞여 있어요
- 포매터·린터의 `--write`/`--fix` 는 **소유 파일 경로만** 인자로 줘요 (`biome check --write <file…>`).
디렉토리·글롭을 주면 명령 하나로 다른 레인 소유 파일을 고쳐요 — 실제로 `src/shared` 전체 포맷이 바뀐 적이 있어요
- 브라우저 세션은 레인 고유 이름을 붙여요 (`--session lane-<이름>`). `close --all` 이나 전역 kill 은
다른 레인의 세션까지 닫아요
- 빌드 출력 경로(DerivedData · `dist` · 캐시)는 브리프의 `## 공용 자원` 이 정한 경로를 써요. 레인마다 새로
잡으면 캐시가 레인 수만큼 쌓여요 — 디스크를 크게 쓰는 작업 전에는 `df -h` 를 보고, 브리프의 기준보다
여유가 적으면 멈추고 보고해요
- **체크박스를 켜기** — `tasks.md` 는 이 레인의 소유 파일이 아니에요. `- [x]` 는 코디네이터가
보고를 받아 원장에 적고(`lanes-dispatch.sh --report`) 검증 명령을 직접 다시 돌린 뒤에 켜요.
레인이 켜면 `tasks-gate.sh` 가 "보고 없이 완료 표시" 로 잡아 spec 완료를 막아요
Expand Down
38 changes: 38 additions & 0 deletions changelog/0.7.6.md
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions changelog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 경고는 커밋을 멈춰야 하는 룰에만 · "집행 사다리" → "막는 방법 우선순위"
Expand Down
4 changes: 3 additions & 1 deletion skills/doctor/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down Expand Up @@ -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-<CAT>-<NNN>: 제목` 의 SSOT 는 `.ax/docs/reference/rules-tokens.md`. 0건이면 본 표 `◆ Cross-cut — Spirit` 줄의 ✅ 만:
Expand All @@ -201,6 +202,7 @@ SL_PH=$(echo "$SL" | jq -r '.result.placeholders[] | "\(.file):\(.line)"')
❗ 비표준 헤더 — <SL_BAD 한 줄씩>
❌ 중복 토큰 — <SL_DUP 한 줄씩>
❗ placeholder 잔재 — <SL_PH 한 줄씩>
❗ 계약(contract:) — <SL_CON 한 줄씩 · 없는 테스트 파일과 contract_ids 로 적었는데 테스트에 안 나오는 ID 만 finding>
```
다음 단계 `[n] ✅ spirit lint 정리 — 비표준 헤더 수정 + 중복 토큰 해소 (사용자와 함께, 자동 수정 X)`. "spirit 점검" 으로 불렸으면 이 절만 보고하고 끝내요. `--strict` 면 placeholder 도 fail.

Expand Down
5 changes: 5 additions & 0 deletions skills/lane/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,11 @@ bash .ax/scripts/bash/tasks-plan.sh --spec "$SPEC" --json
## 금지 파일
<다른 레인 소유 · 공용 설정 · 버전 파일>

## 공용 자원 — 비우지 마세요 (해당 없음이면 "없음")
- 포매터·린터 쓰기: 소유 파일 경로만 인자로 (디렉토리·글롭 금지)
- 브라우저 세션 이름: <lane-이름> — `close --all`·전역 kill 금지
- 빌드 출력 경로: <코디네이터가 정한 한 경로, 예: <scratchpad>/DerivedData> · 디스크 여유 기준: <예: 10GB>

## 규율
- **커밋하지 않아요.** 워킹 트리에 변경만 남기세요. `git add -A` 는 특히 금지 —
동시 세션의 파일까지 쓸어담아요.
Expand Down
2 changes: 1 addition & 1 deletion skills/spec-implement/references/lane-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 와 각 `검증:` 명령 ·
정지 조건 · "커밋하지 않는다" · "체크박스는 켜지 않는다" · 보고 경로. 보고 경로는 레인을 띄우는 방식이 정해요:

| 띄우는 방식 | 레인의 도구 | 보고 경로 | 레인의 최종 응답 |
Expand Down
3 changes: 3 additions & 0 deletions templates/default/.ax/_templates/spirit/rule.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ enforced_by: # 무엇이 이 룰을 실제로 막나 —
# hook:<경로> · external:<도구> · human:<게이트> · TODO:<YYYY-MM-DD>
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/<id>-*.md # 결정 근거 ADR (선택)
---

Expand Down
2 changes: 1 addition & 1 deletion templates/default/.ax/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)이 완료 게이트 미통과면 **한 번** 멈춰 세우고 "마저 하기 · 보류 표기 · 인계 노트" 셋 중 하나를 시켜요 |

Expand Down
34 changes: 34 additions & 0 deletions templates/default/.ax/hooks/pre-commit/spec-completion-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions templates/default/.ax/scripts/bash/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,31 @@ goax_frontmatter_scalar() {
' "$file"
}

# goax_rule_contracts <file>
# 룰 파일 frontmatter 의 `contract:`(그 룰을 막는 짝 테스트 경로)와 `contract_ids:`(계약 대상 SP-ID)를
# `path<TAB>값` · `id<TAB>값` 줄로 내요. 스칼라(`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 <path>
# stdin `<label>\t<glob>` 줄 중 glob 이 path 에 맞는 label 을 처음 나온 순서대로 한 번씩 출력.
# goax_glob_filter 의 반대 방향(경로 하나 × 글롭 여럿)이고 규칙은 goax_glob_match 와 같아요.
Expand Down
Loading
Loading