From fa08512dc9cdc5abc3dde5b152b1df3ea4879dac Mon Sep 17 00:00:00 2001 From: bluecheat Date: Sun, 20 Sep 2026 02:10:09 +0900 Subject: [PATCH] =?UTF-8?q?release:=200.6.2=20=E2=80=94=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EB=A7=88=ED=82=B9=EC=9D=B4=20=EB=B3=B8=EB=AC=B8?= =?UTF-8?q?=EC=97=90=20=EC=96=B8=EA=B8=89=EB=90=9C=20=EB=8B=A4=EB=A5=B8=20?= =?UTF-8?q?task=20=EA=B9=8C=EC=A7=80=20=EC=BC=9C=EB=8D=98=20=EA=B2=83=20(s?= =?UTF-8?q?pec-implement=20=C2=A75=20=EC=9D=B8=EB=9D=BC=EC=9D=B8=20sed=20?= =?UTF-8?q?=E2=86=92=20mark-task.sh)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit spec-implement §5 의 인라인 sed(`.*T013`)가 줄 어디에든 ID 가 있으면 매칭해, 본문에 다른 task 를 언급한 줄까지 [x] 로 뒤집었어요. 실측(statusface spec 024): T005·T007·T009 를 켜자 시작도 안 한 T008·T014·T058 이 검증 없이 완료가 됐고 tasks-gate 는 그걸 정상으로 읽었어요. 락도 없었어요. - 신설 mark-task.sh — 줄 앞 ID 만 매칭(`- [ ] T013 `·`**T013**`·`[T013]`), 코드 펜스 건너뜀, tasks.md.lock, 쓰기 전후 [x] 개수 검증, `--next`(펜스 밖 첫 미완료), `--state '~'`, --dry-run/--json - SKILL §1 NEXT_TASK 가 템플릿 펜스 안 예시(`- [ ] T001 [P] [AC2] <한 줄 설명>`)를 집던 것 → --next - spec-review.sh --merge --json 에 merged_file - README 목록·CLAUDE.md 스크립트 수(38)·changelog 0.6.2·VERSION/plugin/marketplace 0.6.2 검증: 픽스처(펜스 예시·본문 언급 3줄·장식 ID·[~]) 전 경로 + bash tests/smoke.sh 통과 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01RJUj8ng7FfRCctDyceTGyd --- .claude-plugin/marketplace.json | 4 +- .claude-plugin/plugin.json | 2 +- CLAUDE.md | 2 +- VERSION | 2 +- changelog/0.6.2.md | 40 +++++ changelog/README.md | 1 + skills/spec-implement/SKILL.md | 19 +- templates/default/.ax/scripts/bash/README.md | 1 + .../default/.ax/scripts/bash/mark-task.sh | 168 ++++++++++++++++++ .../default/.ax/scripts/bash/spec-review.sh | 3 + 10 files changed, 228 insertions(+), 14 deletions(-) create mode 100644 changelog/0.6.2.md create mode 100644 templates/default/.ax/scripts/bash/mark-task.sh diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 658a227..53c2c23 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.6.1", + "version": "0.6.2", "author": { "name": "bluecheat", "email": "itsinil@gmail.com" @@ -30,5 +30,5 @@ ] } ], - "version": "0.6.1" + "version": "0.6.2" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 75af489..a00a31f 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "goax", - "version": "0.6.1", + "version": "0.6.2", "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/CLAUDE.md b/CLAUDE.md index 12e2f34..194007c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,7 @@ There is no `npm`/`make`/`gradle`. Anything more complex is invoked through Clau - `scripts/provision.sh` — the installer. `up` calls it with `--json`; it reads MANIFEST and copies `templates/default/` into the user project. Lives at the plugin root (not `templates/default/.ax/scripts/`) because it *creates* `.ax/` — it cannot live inside what it installs. Root `install.sh` is an unrelated legacy stub that only prints install instructions. - `templates/default/` — **everything that gets copied into a user project** when the `up` skill runs. - `templates/default/MANIFEST` — installer SSOT. `up` reads this line-by-line; lines ending in `/` are recursive directory copies, `src -> dst` lines do rename copies. **When you add anything under `templates/default/`, update MANIFEST or the installer will not copy it.** Conditional copies (CLAUDE.md, settings.json, config.yml, mistakes/README.md, .gitignore) are intentionally outside MANIFEST and live in `skills/up/SKILL.md` §5–§6. -- `templates/default/.ax/scripts/bash/` — deterministic shell tooling (37 scripts). All follow the standard in `templates/default/.ax/scripts/bash/README.md`: `set -euo pipefail`, `--json --dry-run --help` options, `[goax]` stderr prefix, exit codes `0` ok / `1` error / `2` skipped. Their `--json` shape is `{status, result, next_step, warnings, errors}`. SKILL.md files invoke these and parse the JSON; they do not reimplement the logic inline. +- `templates/default/.ax/scripts/bash/` — deterministic shell tooling (38 scripts). All follow the standard in `templates/default/.ax/scripts/bash/README.md`: `set -euo pipefail`, `--json --dry-run --help` options, `[goax]` stderr prefix, exit codes `0` ok / `1` error / `2` skipped. Their `--json` shape is `{status, result, next_step, warnings, errors}`. SKILL.md files invoke these and parse the JSON; they do not reimplement the logic inline. - `templates/default/.ax/hooks/{user-prompt,pre-bash,pre-edit,post-edit,pre-commit,subagent-start,stop}/*.sh` — sensors. The companion `.claude/settings.json.template` registers them; `doctor` cross-checks installed hooks against this template (it is the SSOT for which hooks *and which event keys* should be registered). `subagent-start/harness-pointer.sh` hands Constitution/Spirit/active-spec/handoff **pointer** to every non-goax subagent (the harness used to stop at the main session); `stop/spec-gate.sh` blocks the end of a turn once when an `implementing`/`review` spec fails `tasks-gate.sh`, unless `current-task.json.handoff.now` records it within 24h (`now_at`) (bounded by `stop_hook_active` and an 8-per-session counter). The two pre-edit injection hooks dedupe per session via `goax_inject_fresh` (`.ax/.session//`, 4h TTL) — measured 1,200 repeat injections / 529KB in one real session before this. - `templates/default/.ax/spirit/{values.md,tone.md,README.md}` — cross-cut Spirit (shared agent personality), the trio the plugin ships. `spirit/rules/` is user-curated (no plugin-shipped instances) — categories get added via onboarding/audit, seeded from `_templates/spirit/ops.md` as an opt-in baseline. Spirit rule headers must match `## SP--: text` — `doctor` lints this. - `templates/default/.ax/_templates/{spec,adr,module,spirit,mistakes}/` — SDD templates the user copies into their own work. `_templates/spec/.origin` is a sha snapshot the installer writes; `check-templates-drift.sh` diffs current vs `.origin` to detect user edits vs plugin updates. diff --git a/VERSION b/VERSION index ee6cdce..b616048 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.6.1 +0.6.2 diff --git a/changelog/0.6.2.md b/changelog/0.6.2.md new file mode 100644 index 0000000..0c164d0 --- /dev/null +++ b/changelog/0.6.2.md @@ -0,0 +1,40 @@ +# 0.6.2 — 완료 마킹이 본문에 언급된 다른 task 까지 켜던 것 (spec-implement §5 인라인 sed → `mark-task.sh`) + +## 달라지는 것 + +> 체감 범위: `spec-implement` 로 task 를 하나씩 끝내는 모든 프로젝트. tasks.md 본문이 다른 task 를 언급하는 +> 게 권장 관행("T009 가 해소" · "T005 의 상수와 맞춤")이라, 그런 줄이 있는 spec 은 전부 해당해요. + +- **완료 마킹이 그 task 한 줄만 켜요.** 전에는 §5 의 인라인 sed 가 `.*T013` 으로 줄 어디에든 ID 가 있으면 + 매칭해서, 아직 시작도 안 한 task 의 본문에 `T013` 이 적혀 있으면 그 줄까지 `[x]` 로 뒤집혔어요. 실측 — + T005·T007·T009 셋을 켜자 T008·T014·T058 이 검증 없이 완료가 됐고, `tasks-gate.sh` 는 그걸 정상으로 + 읽었어요(코디네이터가 켠 체크박스는 원장 없이도 정상이라 구분이 없어요). 이제 `mark-task.sh` 가 **줄 앞** + 의 ID 만 보고(`- [ ] T013 ` · `**T013**` · `[T013]`), 코드 펜스 안은 건너뛰고, 쓰기 전후 `[x]` 개수가 + 정확히 1 늘었는지 확인해요 — 어긋나면 error 로 멈춰요. +- **"다음 task" 가 템플릿 예시를 집지 않아요.** §1 의 `grep -m1 '^- \[ \]'` 는 tasks.md 템플릿의 "한 줄 형식" + 코드 펜스 안 예시(`- [ ] T001 [P] [AC2] <한 줄 설명>`)를 첫 미완료 task 로 돌려줬어요. `mark-task.sh --next` + 는 펜스 밖 줄만 봐요. +- **마킹이 락을 잡아요.** 인라인 sed 는 `tasks.md.lock` 을 안 잡아서 같은 파일을 쓰는 `lanes-dispatch.sh`· + `tasks-gate.sh` 와 lost-update 가 날 수 있었어요. writer 계약(README §쓰기 규약)대로 같은 락 문자열을 써요. +- `spec-review.sh --merge --json` 이 합본 경로 `merged_file` 을 돌려줘요 (텍스트 모드만 찍던 것). + +## 메인테이너용 + +- 결정론 경계 원칙(CLAUDE.md §Architecture 1) 위반이 하나 더 스크립트로 내려갔어요 — SKILL.md 에 남아 있던 + 마지막 `tasks.md` 인라인 writer 예요. +- `mark-task.sh` 는 `--state '~'` 로 의도적 보류도 켜요(사유는 손으로). bash `case` 패턴의 맨 `~` 는 홈으로 + 틸드 확장돼서 `"~"` 로 묶었어요. + +## 세부 (스크립트) + +- 신설 `templates/default/.ax/scripts/bash/mark-task.sh` — `--task T013 [--state x|~] [--dry-run] [--json]` · + `--next [--json]`. 표준 옵션·`goax_help`·`goax_lock`·`goax_mktemp`·exit 0/1/2. +- `skills/spec-implement/SKILL.md` §1 `NEXT_TASK` · §5 마킹 → 스크립트 호출. +- `templates/default/.ax/scripts/bash/README.md` 목록 1행 · `spec-review.sh` JSON `merged_file`. + +## 검증 + +- 픽스처(펜스 예시 + 본문 언급 3줄 + `**T014**`·`[T058]` 장식 + `[~]`): `--next` 가 펜스 밖 T001 · T009 마킹 + 뒤 T008 그대로 · T005 마킹 뒤 T014·T058 그대로 · idempotent `changed:false` · 없는 ID error · `--state '~'` · + `--dry-run` 무변경 · 락 파일 잔존 0. +- `bash tests/smoke.sh` 통과. diff --git a/changelog/README.md b/changelog/README.md index 9bce35a..5821c0c 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -4,6 +4,7 @@ ## 버전 +- [0.6.2](0.6.2.md) — 완료 마킹이 본문에 언급된 다른 task 까지 켜던 것 (`spec-implement` §5 인라인 sed → `mark-task.sh`, 펜스 안 예시를 다음 task 로 집던 것, 마킹 락) - [0.6.1](0.6.1.md) — external 룰의 "자동 실행 안 됨" 경고(I6)가 git 프로젝트에서 한 번도 안 뜨던 것 + 임시 파일·git 이 막힌 환경에서 doctor·`up` 이 틀리게 통과하던 것 (일반 환경 체감 없음) + eval 이 hook·스크립트 층까지 - [0.6.0](0.6.0.md) — 룰의 `검출 패턴:` 마커를 실제로 집행 (grep 룰의 승격 = 집행) - [0.5.13](0.5.13.md) — `spec-completion-gate.sh` 가 spec 없는 triage 직후 매 커밋마다 jq 에러를 찍던 것 diff --git a/skills/spec-implement/SKILL.md b/skills/spec-implement/SKILL.md index 4b5ad2e..ecd2d30 100644 --- a/skills/spec-implement/SKILL.md +++ b/skills/spec-implement/SKILL.md @@ -26,8 +26,11 @@ for f in spec.md tasks.md; do [ ! -f "$SPEC_DIR/$f" ] && { echo "$f 누락 — 먼저 작성"; exit 1; } done -# 다음 미완료 task 찾기 — 없으면 끝난 게 아니라 §8 완료 게이트로 가요 (게이트가 완료를 판정해요) -NEXT_TASK=$(grep -m1 '^- \[ \]' "$SPEC_DIR/tasks.md" || true) +# 다음 미완료 task 찾기 — 없으면 끝난 게 아니라 §8 완료 게이트로 가요 (게이트가 완료를 판정해요). +# 맨 grep 을 쓰지 않아요 — 템플릿 "한 줄 형식" 코드 펜스 안의 예시(`- [ ] T001 [P] [AC2] <한 줄 설명>`)를 +# 다음 task 로 집어요. mark-task.sh --next 는 펜스 밖 줄만 봐요. +NEXT=$(bash .ax/scripts/bash/mark-task.sh --spec "$SPEC" --next --json) +NEXT_TASK=$(printf '%s' "$NEXT" | jq -r '.result.task // empty') [ -z "$NEXT_TASK" ] && echo "미완료 task 없음 → §8 완료 게이트로" ``` @@ -204,13 +207,11 @@ RECURRENCE=$(grep -lE "^category:.*\\b${TASK_DOMAIN}\\b" .ax/mistakes/*.md 2>/de # 방금 끝낸 task 의 ID 를 변수로 — 예시값을 그대로 쓰면 엉뚱한 task 가 체크돼요. TASK_ID="$COMPLETED_TASK_ID" # 예: T013 -# 체크박스만 뒤집어요. task 줄 형식이 `**T013**`·`[T013]`·`T013 [P]` 중 무엇이든 -# ID 만 찾으면 되도록 했어요 (출고 템플릿은 `- [ ] **T013** — ...` 형식). -# ID 뒤에 영숫자가 오면 매칭 안 함 → T001 이 T0011 을 건드리지 않아요. -# `\b` 는 GNU sed 전용이라 안 써요 (BSD/macOS 비호환). -sed -E "/^- \[ \] .*${TASK_ID}([^0-9A-Za-z]|\$)/ s/^- \[ \]/- [x]/" \ - "$SPEC_DIR/tasks.md" > "$SPEC_DIR/tasks.md.tmp" \ - && mv "$SPEC_DIR/tasks.md.tmp" "$SPEC_DIR/tasks.md" +# 체크박스는 mark-task.sh 가 켜요 — 줄 **앞** 의 ID 만 보고(`- [ ] T013 `·`**T013**`·`[T013]`), +# 코드 펜스 안은 건너뛰고, tasks.md.lock 을 잡고, 쓰기 전후 [x] 개수가 정확히 1 늘었는지 확인해요. +# 인라인 sed(`.*T013`)로 하면 본문에 다른 task 를 언급한 줄("T009 가 해소")까지 켜져요 — 실측 3건. +bash .ax/scripts/bash/mark-task.sh --spec "$SPEC" --task "$TASK_ID" --json +# 의도적 보류는 --state '~' (사유는 task 줄에 `보류: <사유>` 로 손으로 적어요) ``` 각 task 완료 후 1줄 보고 (대화 [y/n] X): diff --git a/templates/default/.ax/scripts/bash/README.md b/templates/default/.ax/scripts/bash/README.md index 82240d3..dece983 100644 --- a/templates/default/.ax/scripts/bash/README.md +++ b/templates/default/.ax/scripts/bash/README.md @@ -38,6 +38,7 @@ | `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` | | `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` | diff --git a/templates/default/.ax/scripts/bash/mark-task.sh b/templates/default/.ax/scripts/bash/mark-task.sh new file mode 100644 index 0000000..c2acf52 --- /dev/null +++ b/templates/default/.ax/scripts/bash/mark-task.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +# .ax/scripts/bash/mark-task.sh — tasks.md 체크박스 하나를 켜요 (접두 정확 일치 · 락 · 결과 검증) +# +# Usage: +# bash mark-task.sh [--spec ] --task T013 [--state x|~] [--dry-run] [--json] +# bash mark-task.sh [--spec ] --next [--json] +# +# 왜 필요한가 — spec-implement §5 가 인라인 sed 로 체크박스를 켰어요: +# sed -E "/^- \[ \] .*T013([^0-9A-Za-z]|$)/ s/^- \[ \]/- [x]/" +# `.*` 때문에 "줄 어디에든" ID 가 있으면 매칭돼서, 본문에 다른 task 를 언급한 줄 +# ("T009 가 셋 다 해소" · "T005 의 상수와 맞춤") 까지 같이 켜졌어요 — 실측 3건이 검증 없이 +# 완료로 바뀌었고 게이트는 그걸 정상으로 읽었어요. 그리고 그 sed 는 락이 없어서 +# 같은 파일을 쓰는 lanes-dispatch.sh·tasks-gate.sh 와 lost-update 가 날 수 있었어요. +# +# 이 스크립트는 셋을 고쳐요: +# 1. 매칭은 **줄 앞** — `- [ ] T013 ` / `- [ ] **T013**` / `- [ ] [T013]` 만. 본문 언급은 안 봐요 +# 2. 코드 펜스(```) 안 줄은 건너뛰어요 — 템플릿 예시 `- [ ] T001 [P] [AC2] <한 줄 설명>` 이 걸리지 않게 +# 3. tasks.md.lock 을 잡고 쓰고, 쓰기 전후 `[x]` 개수 차이가 정확히 1(또는 idempotent 0)인지 확인해요 +# +# --next 는 읽기 전용 — 펜스 밖 첫 미완료 task 줄을 돌려줘요 (spec-implement §1 의 NEXT_TASK). +# +# Output (--json): +# --task: {"status":"ok","result":{"spec":"…","task":"T013","state":"x","line":57,"changed":true,"done_before":11,"done_after":12}} +# --next: {"status":"ok","result":{"spec":"…","task":"T014","line":63,"text":"- [ ] T014 …"}} · 없으면 status skipped +# Exit: 0 ok · 1 error (task 없음 · ID 중복 · 락 실패 · 검증 불일치) · 2 skipped (--next 에서 미완료 없음) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "$SCRIPT_DIR/common.sh" + +SPEC=""; TASK=""; STATE="x"; MODE="mark"; DRY_RUN=false; JSON_MODE=false +while [ $# -gt 0 ]; do + case "$1" in + --spec) SPEC="${2:-}"; shift ;; + --task) TASK="${2:-}"; shift ;; + --state) STATE="${2:-}"; shift ;; + --next) MODE="next" ;; + --dry-run) DRY_RUN=true ;; + --json) JSON_MODE=true ;; + -h|--help) goax_help "${BASH_SOURCE[0]}"; exit "$EXIT_OK" ;; + *) goax_error "알 수 없는 옵션: $1"; exit "$EXIT_ERROR" ;; + esac + shift +done + +PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(find_project_root 2>/dev/null || pwd)}" + +fail() { + if [ "$JSON_MODE" = true ]; then json_error "$1"; fi + goax_error "$1"; exit "$EXIT_ERROR" +} + +if [ -z "$SPEC" ] && command -v jq >/dev/null 2>&1; then + TF="$PROJECT_ROOT/.ax/current-task.json" + if [ -f "$TF" ]; then + SD=$(jq -r '.spec_dir // empty' "$TF" 2>/dev/null || true) + [ -n "$SD" ] && SPEC=$(basename "$SD") + fi +fi + +if [ -n "$SPEC" ]; then + RESOLVED=$(goax_resolve_spec "$SPEC" "$PROJECT_ROOT/.ax/docs/spec") && RC=0 || RC=$? + if [ "$RC" -eq 0 ]; then SPEC="$RESOLVED" + elif [ "$RC" -eq 2 ]; then + fail "--spec '$SPEC' 이 여러 spec 에 걸려요: ${GOAX_SPEC_CANDIDATES} — 하나를 정확히 적으세요" + fi +fi + +TASKS="$PROJECT_ROOT/.ax/docs/spec/$SPEC/tasks.md" +LOCK="$TASKS.lock" +if [ -z "$SPEC" ] || [ ! -f "$TASKS" ]; then + fail "tasks.md 를 찾을 수 없어요: .ax/docs/spec/${SPEC:-<없음>}/tasks.md" +fi + +# task 줄 판정 — 펜스 밖 · 줄 앞이 체크박스 · 바로 뒤가 ID (장식 `**`·`[ ]` 허용) · 그 뒤가 비영숫자 +# ID 뒤에 영숫자가 오면 매칭 안 함 → T001 이 T0011 을 건드리지 않아요. `\b` 는 GNU 전용이라 안 써요 +task_line_regex() { + # $1 = 상태 클래스(예: "[ ]" 또는 "[x ~]"), $2 = task ID + printf '^- \\[%s\\] (\\*\\*|\\[)?%s(\\*\\*|\\])?([^[:alnum:]]|$)' "$1" "$2" +} + +# 펜스 밖 줄만 "번호내용" 으로 — awk 는 ``` 를 만날 때마다 토글해요 +outside_fences() { + awk 'BEGIN{f=0} /^```/{f=!f; next} !f {printf "%d\t%s\n", NR, $0}' "$TASKS" +} + +if [ "$MODE" = "next" ]; then + HIT=$(outside_fences | grep -m1 -E $'\t''- \[ \] ' || true) + if [ -z "$HIT" ]; then + if [ "$JSON_MODE" = true ]; then json_skip "미완료 task 없음 — §8 완료 게이트로"; fi + goax_log "미완료 task 없음 — §8 완료 게이트로"; exit "$EXIT_SKIPPED" + fi + LINE=${HIT%%$'\t'*}; TEXT=${HIT#*$'\t'} + ID=$(printf '%s' "$TEXT" | sed -E 's/^- \[ \] (\*\*|\[)?(T[0-9]+).*/\2/') + if [ "$JSON_MODE" = true ]; then + RESULT=$(jq -nc --arg spec "$SPEC" --arg task "$ID" --argjson line "$LINE" --arg text "$TEXT" \ + '{spec:$spec, task:$task, line:$line, text:$text}') + json_output "ok" "$RESULT" "다음 task: $ID" + else + printf '%s\n' "$TEXT" + fi + exit "$EXIT_OK" +fi + +[ -n "$TASK" ] || fail "--task 이 필요해요" +case "$STATE" in x|"~") ;; *) fail "--state 는 x(완료) 또는 ~(보류) 만 받아요: $STATE" ;; esac +printf '%s' "$TASK" | grep -Eq '^T[0-9]+$' || fail "task ID 형식이 아니에요: $TASK (예: T013)" + +ANY_RE=$(task_line_regex '[ x~]' "$TASK") +OPEN_RE=$(task_line_regex '[ ]' "$TASK") + +# 락 안에서 읽어요 — 읽기가 "이 실행이 쓰는가" 를 결정하니 창은 여기서 열려요 +goax_lock "$LOCK" "${GOAX_LOCK_TIMEOUT:-10}" || fail "tasks.md 락을 못 잡았어요: $LOCK — 다른 프로세스가 쓰는 중이거나 락이 남아 있어요" +trap 'goax_unlock "$LOCK"' EXIT + +ALL_HITS=$(outside_fences | grep -E $'\t'"${ANY_RE#^}" || true) +N_ALL=$(printf '%s\n' "$ALL_HITS" | grep -c . || true); N_ALL=${N_ALL:-0} +[ "$N_ALL" -ge 1 ] || fail "task $TASK 줄이 없어요 (펜스 밖 · 줄 앞 ID 기준) — tasks.md 형식은 '- [ ] T0NN …' 이에요" +[ "$N_ALL" -eq 1 ] || fail "task $TASK 줄이 ${N_ALL}개예요 — ID 가 중복됐어요. 하나만 남기세요" + +LINE=${ALL_HITS%%$'\t'*} +TEXT=${ALL_HITS#*$'\t'} +DONE_BEFORE=$(grep -cE '^- \[x\] ' "$TASKS" || true); DONE_BEFORE=${DONE_BEFORE:-0} + +if ! printf '%s' "$TEXT" | grep -Eq "$OPEN_RE"; then + # 이미 [x] 또는 [~] — 다시 켤 게 없어요 (idempotent). 상태가 다르면 알려만 줘요 + CUR=$(printf '%s' "$TEXT" | sed -E 's/^- \[(.)\].*/\1/') + if [ "$JSON_MODE" = true ]; then + RESULT=$(jq -nc --arg spec "$SPEC" --arg task "$TASK" --arg state "$CUR" --argjson line "$LINE" \ + --argjson before "$DONE_BEFORE" '{spec:$spec, task:$task, state:$state, line:$line, changed:false, done_before:$before, done_after:$before}') + json_output "ok" "$RESULT" "이미 [$CUR] 예요 — 바꾸지 않았어요" + else + goax_log "$TASK 는 이미 [$CUR] 예요 — 바꾸지 않았어요" + fi + exit "$EXIT_OK" +fi + +if [ "$DRY_RUN" = true ]; then + if [ "$JSON_MODE" = true ]; then + RESULT=$(jq -nc --arg spec "$SPEC" --arg task "$TASK" --arg state "$STATE" --argjson line "$LINE" \ + --argjson before "$DONE_BEFORE" '{spec:$spec, task:$task, state:$state, line:$line, changed:false, dry_run:true, done_before:$before}') + json_output "ok" "$RESULT" "dry-run — ${LINE}행을 [$STATE] 로 바꿀 거예요" + else + printf '(dry-run) %s행: %s → [%s]\n' "$LINE" "$TEXT" "$STATE" + fi + exit "$EXIT_OK" +fi + +tmp=$(goax_mktemp "$PROJECT_ROOT") || { goax_tmp_error; exit "$EXIT_ERROR"; } +# 그 한 줄만 바꿔요 — 줄 번호로 집어요 (같은 ID 가 둘이면 위에서 이미 막았어요) +awk -v n="$LINE" -v st="$STATE" 'NR==n { sub(/^- \[ \]/, "- [" st "]") } { print }' "$TASKS" > "$tmp" \ + && mv "$tmp" "$TASKS" || { rm -f "$tmp"; fail "tasks.md 갱신 실패: $TASKS"; } + +# 사후 검증 — 정확히 한 줄만 바뀌었는지. 한 줄 매칭인데 개수가 2 이상 뛰면 awk 가 잘못 집은 거예요 +DONE_AFTER=$(grep -cE '^- \[x\] ' "$TASKS" || true); DONE_AFTER=${DONE_AFTER:-0} +EXPECT=$DONE_BEFORE; [ "$STATE" = "x" ] && EXPECT=$((DONE_BEFORE + 1)) +[ "$DONE_AFTER" -eq "$EXPECT" ] || fail "마킹 뒤 [x] 개수가 어긋나요: 전 $DONE_BEFORE → 후 $DONE_AFTER (기대 $EXPECT). tasks.md 를 눈으로 확인하세요" + +if [ "$JSON_MODE" = true ]; then + RESULT=$(jq -nc --arg spec "$SPEC" --arg task "$TASK" --arg state "$STATE" --argjson line "$LINE" \ + --argjson before "$DONE_BEFORE" --argjson after "$DONE_AFTER" \ + '{spec:$spec, task:$task, state:$state, line:$line, changed:true, done_before:$before, done_after:$after}') + json_output "ok" "$RESULT" "$TASK → [$STATE] (${DONE_AFTER} 완료)" +else + goax_log "✓ $TASK → [$STATE] (${LINE}행, 완료 ${DONE_BEFORE} → ${DONE_AFTER})" +fi +exit "$EXIT_OK" diff --git a/templates/default/.ax/scripts/bash/spec-review.sh b/templates/default/.ax/scripts/bash/spec-review.sh index f45cb3f..5716cb9 100755 --- a/templates/default/.ax/scripts/bash/spec-review.sh +++ b/templates/default/.ax/scripts/bash/spec-review.sh @@ -235,6 +235,7 @@ EOF PASS=true; REASON="둘 다 진행 · sha 일치 (round ${ROUND})" fi + OUT="" if [ "$MODE" = "merge" ] && [ "$DRY_RUN" != true ]; then OUT="$SPEC_DIR/review-spec.md" { @@ -256,8 +257,10 @@ EOF --argjson ap "$A_P" --arg av "$A_V" --arg as "$A_S" --argjson am "$A_M" --arg af "$A_FILE" \ --argjson ep "$E_P" --arg ev "$E_V" --arg es "$E_S" --argjson em "$E_M" --arg ef "$E_FILE" \ --argjson pass "$PASS" --arg reason "$REASON" --arg mode "$MODE" --argjson dry "$DRY_RUN" \ + --arg merged "${OUT:-}" \ '{spec:$spec, sha:$sha, spec_sha:$ssha, tasks_sha:(if $tsha=="" then null else $tsha end), required:$req, required_source:$reqsrc, round:$round, mode:$mode, dry_run:$dry, + merged_file:(if $merged=="" then null else $merged end), changed_files:$ch, architect:{present:$ap, verdict:(if $av=="" then null else $av end), sha:(if $as=="" then null else $as end), sha_match:$am, file:$af}, evaluator:{present:$ep, verdict:(if $ev=="" then null else $ev end), sha:(if $es=="" then null else $es end), sha_match:$em, file:$ef},