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.3",
"version": "0.7.4",
"author": {
"name": "bluecheat",
"email": "itsinil@gmail.com"
Expand All @@ -30,5 +30,5 @@
]
}
],
"version": "0.7.3"
"version": "0.7.4"
}
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.3",
"version": "0.7.4",
"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.3
0.7.4
27 changes: 27 additions & 0 deletions changelog/0.7.4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# 0.7.4 — 레인의 일부만 맡겨도 원장이 그대로 적어요

## 무엇이 좋아졌나 — 누구에게 체감되나

- **라운드마다 실제로 맡긴 task 만 디스패치로 적혀요.** 예전엔 `--dispatch` 가 레인 이름만 받아서, 그 레인의 미완료
task 전부에 시각을 찍었어요. 한 실사용 프로젝트(레인 5개 · 워커 4개 동시)에서 한 레인의 8 task 가 실제 기동보다 77분
먼저 디스패치로 적혔고, 아무도 안 맡은 4 task 가 `dispatched_unreported` 와 완료 게이트의 "보고 안 받은 디스패치" 에
섞여 경보가 흐려졌어요. 이제 `--dispatch T010,T011` 로 그 task 들만 적어요. `--report` 와 같은 모양이에요.
- **여러 task 를 한 번에 완료 표시해요.** 레인 보고 하나로 여러 task 의 검증이 통과했을 때 `mark-task.sh --task T010,T011`.

## 바뀐 것

- `lanes-dispatch.sh --dispatch <레인 | T010,T011>`
- 레인 이름이면 기존 동작 그대로예요.
- task 목록이면 전부 미완료 · 같은 레인에 배정돼 있어야 해요. 아니면(없는 ID · 완료/보류 · 레인 없음 · 여러 레인)
exit 1 이고 원장은 그대로예요 (`--assign` 과 같은 트랜잭션).
- 이미 디스패치된 task 를 다시 적으면 시각을 갱신하고 `보고:` 를 지운 뒤 `warnings` 에 적어요. 중복 ID 는 한 번만.
- 파일 소유 충돌 검사는 그 task 들이 속한 레인 기준이에요.
- `mark-task.sh --task T010,T011` — 트랜잭션(하나라도 없거나 중복이면 무변경), 이미 켜진 것은 `unchanged_tasks`.
result 에 `tasks` · `lines` · `changed_tasks` · `unchanged_tasks` 를 더했어요. 단일 ID 의 기존 필드는 그대로예요.
- 문서: `spec-implement/references/lane-mode.md` §2 가 task 목록 디스패치를 기본으로 안내해요. `lane` §6 · `spec-implement` §5 ·
스크립트 README 도 같이 고쳤어요.

## 검증

- smoke 738 통과 (macOS bash 3.2). §38 에 task 목록 디스패치 3건(목록만 기록 · 잘못된 목록 4종 거부+무변경 · 재디스패치 경고),
§59 에 다중 ID 2건(부분 적용 없음 · 이미 켜진 것 건너뜀).
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.4](0.7.4.md) — 레인의 일부만 맡겨도 원장이 그대로 적어요 (`--dispatch T010,T011` · `mark-task --task T010,T011`)
- [0.7.3](0.7.3.md) — audit 이 경고만 있는 룰을 만들던 것: 룰 본문은 가이드("어떻게 해야 하나")부터, grep 경고는 커밋을 멈춰야 하는 룰에만 · "집행 사다리" → "막는 방법 우선순위"
- [0.7.2](0.7.2.md) — task 하나를 끝냈는데 본문에 이름만 나온 다른 task 까지 완료로 바뀌던 것 (`mark-task.sh`)
- [0.7.1](0.7.1.md) — lint 결과가 모델에게 닿고, 품질 설정 완화는 이유부터, 압축 뒤에도 위치를 기억해요 · 응답 이모지 정리
Expand Down
2 changes: 2 additions & 0 deletions skills/lane/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ bash .ax/scripts/bash/lanes-dispatch.sh --spec "$SPEC" --assign "T010=A,T011=A,T

`result.lane_file_conflicts` 가 비어야 해요 — 같은 파일을 두 레인이 갖고 있으면 나중에
`--dispatch` 가 거부돼요. 레인 이름은 짧게(A·B·C 또는 역할명), task 하나는 레인 하나예요.
디스패치는 `spec-implement` 가 라운드마다 해요 — `--dispatch T010,T011` 로 그 라운드에 실제로 맡긴
task 만 적고(같은 레인이어야 해요), `--dispatch A` 는 레인의 미완료 전부를 한 번에 맡길 때만 써요.
옮기려면 `--force`. 배정하지 않은 task 는 코디네이터가 직접 하는 일이에요 — 떼어낸 배럴
task(정책 B)가 보통 여기예요.

Expand Down
1 change: 1 addition & 0 deletions skills/spec-implement/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ TASK_ID="$COMPLETED_TASK_ID" # 예: T013
# 인라인 sed(`.*T013`)는 본문에 다른 task 를 언급한 줄("T009 가 해소")까지 켰어요.
bash .ax/scripts/bash/mark-task.sh --spec "$SPEC" --task "$TASK_ID" --json
# 의도적 보류는 --state '~' (사유는 task 줄에 `보류: <사유>` 로 적어요)
# 레인 보고로 여러 task 가 한꺼번에 검증을 통과했으면 --task T010,T011 — 하나라도 없으면 아무것도 안 켜요
```

각 task 완료 후 1줄 보고 (대화 [y/n] X):
Expand Down
15 changes: 12 additions & 3 deletions skills/spec-implement/references/lane-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,22 @@ echo "$LEDGER" | jq -r '.result.unassigned_open | join(" ")'
- §3.2 strict 조건(L3 + SP-SEC/DATA 등)에 걸리는 task 는 **레인에 넘기지 않아요.**
코디네이터가 `[y/n]` 을 받고 직접 해요

**2. 디스패치를 원장에 먼저 적고, 그다음 띄워요.**
**2. 디스패치를 원장에 먼저 적고, 그다음 띄워요.** 원장엔 **이번 브리프에 실제로 넣은 task 만** 적어요.

```bash
bash .ax/scripts/bash/lanes-dispatch.sh --spec "$SPEC" --dispatch A --json # 파일 소유 충돌이면 여기서 거부돼요
bash .ax/scripts/bash/lanes-dispatch.sh --spec "$SPEC" --dispatch T010,T011 --json # 이번 라운드에 맡기는 task 만
bash .ax/scripts/bash/lanes-dispatch.sh --spec "$SPEC" --dispatch A --json # 레인의 미완료 전부를 한 번에 맡길 때
```

**exit 1 이 항상 에러는 아니에요** — 그 레인의 미완료 task 가 전부 이미 디스패치·보고까지
- **task 목록이 기본이에요.** §1 의 "ready ∩ 레인 A" 는 보통 레인 A 의 일부라서, `--dispatch A` 로 적으면
아무도 안 맡은 task 까지 시각이 찍혀요. 그러면 `dispatched_unreported` 와 완료 게이트의 "보고 안 받은
디스패치" 에 안 보낸 task 가 섞여 신호가 흐려져요 (실측: 레인 하나의 8 task 가 실제 기동보다 77분 먼저
찍혔고, 아무도 안 맡은 4 task 가 경보에 들어갔어요)
- 목록은 전부 미완료 · 같은 레인이어야 해요. 아니면 exit 1 이고 원장은 그대로예요
- 이미 디스패치된 task 를 다시 적으면 시각을 갱신하고 `보고:` 를 지운 뒤 `warnings` 에 알려요 — 재전송이에요
- 파일 소유 충돌이면 둘 다 여기서 거부돼요 (그 task 들이 속한 레인 기준)

**exit 1 이 항상 에러는 아니에요** — 레인 이름으로 보냈는데 그 레인의 미완료 task 가 전부 이미 디스패치·보고까지
끝났으면 (더 보낼 게 없으면) `--dispatch` 가 exit 1 로 끝나요 (`status: error` + "전부 보고까지
받았어요 — 그래도 다시 보내려면 --force"). 이건 종료 조건이지 실패가 아니에요 — §5 로 넘어가서
`ready`·`dispatched_unreported` 를 다시 봐요. 진짜 실패(파일 소유 충돌)는 메시지가 달라요.
Expand Down
4 changes: 2 additions & 2 deletions templates/default/.ax/scripts/bash/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
| `tasks-gate.sh` | spec 완료 게이트 G1~G6 — 미완료 · AC 커버리지 · orphan · 유실 · 레인 원장 · evaluator verdict(`review.md`) | `spec-implement`, `lane`, pre-commit hook |
| `lanes-hotfiles.sh` | tasks.md 에서 핫 파일(여러 미완료 task 가 쓰는 파일) + `files:` 누락 task 추출 | `lane` |
| `spec-review.sh` | spec 합의 리뷰 원장 — `--snapshot`(sha 고정·라운드) / `--status`(리뷰어별 `review-spec.{architect,evaluator}.md` 의 verdict·sha 집계 → pass) / `--merge`(합본). 필수 여부는 Size 축만 | `spec-validate` |
| `lanes-dispatch.sh` | 레인 디스패치 원장 — `--assign / --dispatch / --report / --status`. tasks.md 의 `레인:`·`디스패치:`·`보고:` 필드를 쓰고, 파일 소유 충돌이면 dispatch 거부 | `lane`, `spec-implement` |
| `mark-task.sh` | tasks.md 체크박스 하나를 켜요 — `--task T013 [--state x\|~]` · `--next`(펜스 밖 첫 미완료). 줄 **앞** ID 만 매칭(본문 언급 무시), 코드 펜스 건너뜀, `tasks.md.lock`, 쓰기 전후 `[x]` 개수 검증 | `spec-implement` |
| `lanes-dispatch.sh` | 레인 디스패치 원장 — `--assign / --dispatch <레인 \| T010,T011> / --report <레인 \| T010,T011> / --status`. tasks.md 의 `레인:`·`디스패치:`·`보고:` 필드를 쓰고, 파일 소유 충돌이면 dispatch 거부 | `lane`, `spec-implement` |
| `mark-task.sh` | tasks.md 체크박스를 켜요 — `--task T013[,T014] [--state x\|~]` · `--next`(펜스 밖 첫 미완료). 줄 **앞** ID 만 매칭(본문 언급 무시), 코드 펜스 건너뜀, `tasks.md.lock`, 쓰기 전후 `[x]` 개수 검증. 여러 ID 는 트랜잭션(하나라도 없으면 무변경) | `spec-implement` |
| `zero-init.sh` | 0→1 첫날 팩 설치 (룰은 라이브 `spirit/rules/`, 템플릿은 `_templates/zero/`) — 덮어쓰지 않고 SP 토큰 충돌만 경고 | `zero` |
| `zero-domain-risk.sh` | `config.yml` 의 `domain_risk` 블록 통째 교체 (`--show/--set/--default`) — 출고 예시 키가 남으면 triage 가 영원히 default_risk 로 흘러요 | `zero` |
| `zero-probe.sh` | 네거티브 프로브 — 일부러 위반을 만들어 차단이 실제로 도는지 확인 | `zero` |
Expand Down
56 changes: 46 additions & 10 deletions templates/default/.ax/scripts/bash/lanes-dispatch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Usage:
# bash lanes-dispatch.sh [--spec <id-slug>] [--status] [--json]
# bash lanes-dispatch.sh [--spec <id-slug>] --assign "T010=A,T011=A,T020=B" [--force] [--dry-run] [--json]
# bash lanes-dispatch.sh [--spec <id-slug>] --dispatch <lane> [--force] [--dry-run] [--json]
# bash lanes-dispatch.sh [--spec <id-slug>] --dispatch <lane | T010,T011> [--force] [--dry-run] [--json]
# bash lanes-dispatch.sh [--spec <id-slug>] --report <lane | T010,T011> [--dry-run] [--json]
#
# 왜 필요한가 — 코디네이터의 기억은 컨텍스트 창 안에만 있어요. 어떤 task 를 어느 레인에
Expand All @@ -30,6 +30,12 @@
# --dispatch 는 기본적으로 **아직 보고를 못 받은** 미완료 task 만 보내요. 이미 보고까지
# 받은 task 를 다시 보내려면 --force (그때만 그 task 의 `보고:` 를 지워요).
# --dispatch 는 그 레인이 lane_file_conflicts 에 걸려 있으면 거부해요 (exit 1, --force 로 우회).
# --dispatch T010,T011 처럼 task 목록을 주면 **그 task 들만** 보내요 — 라운드마다 레인의 일부만
# 워커에 맡길 때예요. 레인 이름으로 보내면 그 레인의 미완료 전부에 시각이 찍혀서, 아무도 안 맡은
# task 가 "보고 안 받은 디스패치" 로 세져 경보가 흐려졌어요 (실측: 레인 하나의 8 task 가 실제
# 기동보다 77분 먼저 찍히고, 안 맡긴 4 task 가 경보에 섞임). 목록은 전부 미완료여야 하고 같은
# 레인에 배정돼 있어야 해요 (아니면 exit 1, 파일 무변경). 이미 디스패치된 task 를 다시 지정하면
# 시각을 갱신하고 `보고:` 를 지운 뒤 warnings 에 적어요 — 이름을 직접 댄 것 자체가 재전송 의사예요.
# 소유자는 정하지 않아요 — 배정은 판단이고 lane skill 이 사람과 해요. 여기선 기록과 검사만.
#
# `files:` 경로는 비교 전에 정규화해요 (`./` 제거 · 중복 `/` · 끝 `/`) — `src/a.ts` 와
Expand Down Expand Up @@ -265,16 +271,46 @@ case "$MODE" in
IFS="$oldIFS"
;;
dispatch)
[ -n "$LANE" ] || fail "--dispatch 에 레인 이름이 필요해요"
OPEN_IN_LANE=$(printf '%s\n' "$LEDGER" | awk -F'|' -v l="$LANE" '$2=="open" && $4==l{print $1}')
[ -n "$OPEN_IN_LANE" ] || fail "레인 '$LANE' 에 미완료 task 가 없어요 — --assign 으로 먼저 배정하세요"
if [ "$FORCE" = true ]; then
TARGETS=$(printf '%s\n' "$OPEN_IN_LANE" | paste -sd, - || true)
[ -n "$LANE" ] || fail "--dispatch 에 레인 이름 또는 task ID 목록(T010,T011)이 필요해요"
IS_LANE=$(printf '%s\n' "$LEDGER" | awk -F'|' -v l="$LANE" '$4==l{f=1} END{print f?1:0}')
if [ "$IS_LANE" != "1" ] && printf '%s' "$LANE" | tr -d ' ' | grep -Eq '^T[0-9]+(,T[0-9]+)*,?$'; then
# task 목록 — 전부 검증한 뒤에만 써요 (--assign 과 같은 트랜잭션: 반쯤 찍힌 원장은 더 나빠요)
TARGETS=$(printf '%s' "$LANE" | tr -d ' ' | tr ',' '\n' | awk 'NF && !seen[$0]++' | paste -sd, -)
MISSING=""; NOTOPEN=""; NOLANE=""; LANES_SEEN=""; REDISP=""
oldIFS="$IFS"; IFS=','
for tid in $TARGETS; do
[ -z "$tid" ] && continue
has_id "$tid" || { MISSING="$MISSING$tid "; continue; }
row=$(printf '%s\n' "$LEDGER" | awk -F'|' -v id="$tid" '$1==id{print; exit}')
st=$(printf '%s' "$row" | cut -d'|' -f2)
ln=$(printf '%s' "$row" | cut -d'|' -f4)
dp=$(printf '%s' "$row" | cut -d'|' -f5)
[ "$st" = "open" ] || NOTOPEN="$NOTOPEN$tid($st) "
if [ -z "$ln" ]; then NOLANE="$NOLANE$tid "
else case " $LANES_SEEN " in *" $ln "*) ;; *) LANES_SEEN="${LANES_SEEN:+$LANES_SEEN }$ln" ;; esac
fi
[ -n "$dp" ] && REDISP="$REDISP$tid "
done
IFS="$oldIFS"
[ -n "$MISSING" ] && fail "tasks.md 에 없는 task: ${MISSING% }"
[ -n "$NOTOPEN" ] && fail "미완료가 아닌 task 는 디스패치하지 않아요: ${NOTOPEN% }"
[ -n "$NOLANE" ] && fail "레인이 배정되지 않은 task: ${NOLANE% } — --assign 으로 먼저 배정하세요"
case "$LANES_SEEN" in
*" "*) fail "task 목록이 여러 레인에 걸쳐 있어요 (${LANES_SEEN}) — 디스패치는 레인 하나씩이에요" ;;
esac
LANE="$LANES_SEEN"
[ -n "$REDISP" ] && WARN="이미 디스패치된 task 를 다시 보냈어요: ${REDISP% } — 디스패치 시각을 갱신하고 보고 기록을 지웠어요"
else
# 기본 대상 = 디스패치 기록이 없거나 보고를 못 받은 것. 보고까지 받은 task 는 건드리지 않아요
TARGETS=$(printf '%s\n' "$LEDGER" | awk -F'|' -v l="$LANE" '$2=="open" && $4==l && ($5=="" || $6==""){print $1}' | paste -sd, - || true)
SKIPPED=$(printf '%s\n' "$LEDGER" | awk -F'|' -v l="$LANE" '$2=="open" && $4==l && $5!="" && $6!=""{print $1}' | paste -sd, - || true)
[ -n "$TARGETS" ] || fail "레인 '$LANE' 의 미완료 task 는 전부 디스패치 후 보고까지 받았어요 (${SKIPPED}) — 그래도 다시 보내려면 --force"
OPEN_IN_LANE=$(printf '%s\n' "$LEDGER" | awk -F'|' -v l="$LANE" '$2=="open" && $4==l{print $1}')
[ -n "$OPEN_IN_LANE" ] || fail "레인 '$LANE' 에 미완료 task 가 없어요 — --assign 으로 먼저 배정하세요"
if [ "$FORCE" = true ]; then
TARGETS=$(printf '%s\n' "$OPEN_IN_LANE" | paste -sd, - || true)
else
# 기본 대상 = 디스패치 기록이 없거나 보고를 못 받은 것. 보고까지 받은 task 는 건드리지 않아요
TARGETS=$(printf '%s\n' "$LEDGER" | awk -F'|' -v l="$LANE" '$2=="open" && $4==l && ($5=="" || $6==""){print $1}' | paste -sd, - || true)
SKIPPED=$(printf '%s\n' "$LEDGER" | awk -F'|' -v l="$LANE" '$2=="open" && $4==l && $5!="" && $6!=""{print $1}' | paste -sd, - || true)
[ -n "$TARGETS" ] || fail "레인 '$LANE' 의 미완료 task 는 전부 디스패치 후 보고까지 받았어요 (${SKIPPED}) — 그래도 다시 보내려면 --force"
fi
fi
;;
report)
Expand Down
Loading
Loading