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.5.10",
"version": "0.5.11",
"author": {
"name": "bluecheat",
"email": "itsinil@gmail.com"
Expand All @@ -30,5 +30,5 @@
]
}
],
"version": "0.5.10"
"version": "0.5.11"
}
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.5.10",
"version": "0.5.11",
"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
6 changes: 3 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ There is no `npm`/`make`/`gradle`. Anything more complex is invoked through Clau
- `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 (36 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/STATUS **paths** 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 STATUS.md already records the halt (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/<sid>/`, 4h TTL) — measured 1,200 repeat injections / 529KB in one real session before this.
- `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/<sid>/`, 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-<CAT>-<NNN>: 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.
- `templates/default/CLAUDE.md.template` — the Layer 1 Constitution that lands in user projects (do not confuse with this file).
Expand All @@ -50,13 +50,13 @@ Three things must stay true together or the design breaks:
- Every SKILL.md begins with `## 시작 전 필수` declaring it loads `.ax/spirit/values.md` and `tone.md`. Triage explicitly fails if those are missing.
- Tone throughout the plugin is Korean **`~해요` 체** — this is a deliberate consistency choice (see CONCEPTS §4.1, §7.3) and applies to all user-facing text including SKILL.md bodies, error messages, and ✓ confirmations.
- Skills update `.ax/state.json` (HUD signals) and `.ax/current-task.json` (triage→spec→audit context handoff) at well-defined points. Use the existing helpers (`update-state.sh`, jq tmp-mv pattern) — do not invent new state files.
- When a script does `read → jq/awk → tmp.$$ → mv` on a file more than one script or session can touch concurrently (`tasks.md`, `state.json`, `STATUS.md`, `.claude/settings.json`, `AGENTS.md`, `.ax/config.yml`), wrap the read-modify-write in `common.sh`'s `goax_lock "$LOCK" "${GOAX_LOCK_TIMEOUT:-10}"` / `goax_unlock` — without it, concurrent writers silently lose each other's updates. Four rules keep the lock from being decorative. **The window opens at the first read that decides whether this run mutates** — the idempotent `grep -q` probe, not the `mv`; a probe outside the lock means both processes read "not there yet" and both write (measured: five concurrent `zero-init.sh` registered the same hook 3×). **The lock unit is the target file, not the script** — `constitution-apply.sh` takes the same `<target>.lock` in all three of its modes, and two scripts writing the *same* file (`update-state.sh` ↔ `tasks-gate.sh` on `state.json`, `register-spirit-hook.sh` ↔ `zero-init.sh` on `.claude/settings.json`) must build the identical lock path string, or the lock is split in two and does nothing. **The path must be absolute** — scripts that `cd` to the project root hold a relative target, so normalize it (`goax_normalize_path "$TARGET" "$PROJECT_ROOT"`). **`--dry-run` takes no lock and writes nothing** — the lock directory is itself a side effect, and a dry-run that calls an `ensure_file`-style helper before its dry-run branch is still mutating.
- When a script does `read → jq/awk → tmp.$$ → mv` on a file more than one script or session can touch concurrently (`tasks.md`, `state.json`, `current-task.json`, `.claude/settings.json`, `AGENTS.md`, `.ax/config.yml`), wrap the read-modify-write in `common.sh`'s `goax_lock "$LOCK" "${GOAX_LOCK_TIMEOUT:-10}"` / `goax_unlock` — without it, concurrent writers silently lose each other's updates. Four rules keep the lock from being decorative. **The window opens at the first read that decides whether this run mutates** — the idempotent `grep -q` probe, not the `mv`; a probe outside the lock means both processes read "not there yet" and both write (measured: five concurrent `zero-init.sh` registered the same hook 3×). **The lock unit is the target file, not the script** — `constitution-apply.sh` takes the same `<target>.lock` in all three of its modes, and two scripts writing the *same* file (`update-state.sh` ↔ `tasks-gate.sh` on `state.json`, `register-spirit-hook.sh` ↔ `zero-init.sh` on `.claude/settings.json`, `status-note.sh` ↔ `tier-from-state.sh --reset` on `current-task.json`) must build the identical lock path string, or the lock is split in two and does nothing. **The path must be absolute** — scripts that `cd` to the project root hold a relative target, so normalize it (`goax_normalize_path "$TARGET" "$PROJECT_ROOT"`). **`--dry-run` takes no lock and writes nothing** — the lock directory is itself a side effect, and a dry-run that calls an `ensure_file`-style helper before its dry-run branch is still mutating.
- Never write a new `[a-z]`/`[A-Z]` bracket range in glob or regex matching — under `en_US.UTF-8` collation, `[a-z]` also matches uppercase letters (sort order is `aAbB…zZ`), which has silently passed values like `Payment` through a `case` guard meant to reject them. Use `[[:lower:]]`/`[[:upper:]]`/`[[:alnum:]]`, or rely on `common.sh`'s top-level `export LC_COLLATE=C` as a safety net for code you can't rewrite yet.
- Any parser that walks `tasks.md` line-by-line (task counts, `[P]` conflict checks, ledger fields) must skip fenced code blocks first (`/^[[:space:]]*```/{fence=!fence; next} fence{next}`) — `_templates/spec/tasks.md` itself documents the task-line format inside a fence, and an unguarded parser counts that example as a real task.
- The `${CLAUDE_SKILL_DIR}` env var is the official Claude Code variable for finding the plugin root (`${CLAUDE_SKILL_DIR}/../..`). `${CLAUDE_PLUGIN_ROOT}` exists only as a legacy fallback. Do not introduce other variable names.
- `spec-validate` runs the plan-time consensus review after the deterministic clarity gate: `spec-review.sh --snapshot` pins the spec sha, then `architect` and `evaluator` (spec mode) run **sequentially in fresh contexts, never seeing each other's file**, each writing `review-spec.<role>.md` with a `verdict:` first line; `spec-review.sh --status` aggregates. Required for size L/XL, optional (`--consensus`) for M, never for S — risk is deliberately not a factor (G6 already covers it). Do not let the main session write the reviewer files.
- The HUD (`templates/default/.ax/hud/statusline.sh`) shows harness position only: `[goax#ver] | S×R · domain | spec › tasks › impl › review | mistakes:N`. It reads files only (no subprocess scripts) and depends on `current-task.json.phase` — `spec-implement` writes `implementing` at entry and `review` while waiting for the evaluator; without those writes the chain does not move. Heavier values live in `state.json.hud` written by `update-state.sh` (3-way sync with `state.json.template` and `docs/state-ownership.md`).
- **Session handoff lives in `.ax/docs/STATUS.md`, written only through `status-note.sh`** (`--add|--done|--set <now|next|open|renamed>`, 40-line cap, finished items are deleted — git log and ADRs are the SSOT for what got done). `triage` §1.0 reads it *before* `MEMORY.md`; `spec-implement` writes it on halt, on completion, and when a lane report renames a shared name; `zero` §13 opens it on day one. `MEMORY.md` (`build-memory.sh`) is a rule-token index, not a memory: even in lean mode it keeps the 🔴/🟡 pointers, and only the ADR/spec/module enumerations fold. There is no BM25 index any more (`build-index.sh` was removed in 0.5.3 — no project corpus ever crossed the threshold; bring it back only if `.ax/docs` measures past ~2,000 files).
- **Session handoff lives in `.ax/current-task.json` under `handoff`** (`now[]`·`now_at`·`next[]`·`open[]`·`renamed[]`), written only through `status-note.sh` (`--add|--done|--set <now|next|open|renamed>`, 40-item cap, finished items are deleted — git log and ADRs are the SSOT for what got done); `now_at` is the freshness stamp the Stop gate reads, and `reset-task.sh` leaves `handoff` alone because `next`/`open`/`renamed` outlive a task. `triage` §1.0 reads it *before* `MEMORY.md`; `spec-implement` writes it on halt, on completion, and when a lane report renames a shared name; `zero` §13 opens it on day one. `MEMORY.md` (`build-memory.sh`) is a rule-token index, not a memory: even in lean mode it keeps the 🔴/🟡 pointers, and only the ADR/spec/module enumerations fold. There is no BM25 index any more (`build-index.sh` was removed in 0.5.3 — no project corpus ever crossed the threshold; bring it back only if `.ax/docs` measures past ~2,000 files).
- `doctor` does not count anything inline. Its findings come from scripts: `doctor-scan.sh` (migration residue · hook registration against `settings.json.template` · Constitution-vs-installed mechanism · **reach map** — whether each rule source actually lands in a session: AGENTS.md without a `CLAUDE.md` that imports it reaches nothing), `spirit-lint.sh`, `rules-index.sh`, `check-rule-enforcement.sh`, `check-sensor-liveness.sh`. Add a new check as a script first, then a doctor section that reads its JSON.
- `spec-implement` is the only executor. It runs single-lane (sequential) unless `tasks.md` carries `레인:` ledger fields, in which case it is the coordinator: it stamps `디스패치:` via `lanes-dispatch.sh`, spawns `lane-worker` agents, stamps `보고:` when a report arrives, re-runs the task's `검증:` command itself, and only then flips the checkbox. Lanes never flip checkboxes and never write the ledger. Completion is `tasks-gate.sh` G1–G6; G5 is the ledger, G6 is the fresh-context `evaluator` verdict. The coordinator must not write `review.md` itself. When you add an execution edge (who invokes whom), wire it in the receiving SKILL.md, not just in the sender — the `lane`→`spec-implement` and triage-matrix→`evaluator` handoffs were prose-only until they were wired here.

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.5.10
0.5.11
33 changes: 33 additions & 0 deletions changelog/0.5.11.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 0.5.11 — 인계 노트가 세 번째 상태 파일이던 것

> `.ax/docs/STATUS.md` 는 어느 리포에도 커밋된 적 없는 세션 상태인데 `.gitignore.template` 에 없었고,
> 커밋되는 `.ax/docs/` 트리 한가운데 있었어요. 리포 계약은 "상태 파일은 state.json · current-task.json 둘,
> 새로 만들지 않아요" 예요. STATUS.md 가 그걸 어겼어요.

## 무엇이 바뀌었나

- 인계 노트가 `.ax/current-task.json` 의 `handoff` 객체로 들어가요 — now · now_at · next · open · renamed.
쓰는 길은 그대로 `status-note.sh` 하나 (CLI · `--show --json` 의 sections/counts/over_cap 불변, `lines` → `items`, `now_at` 추가).
- 스탬프가 줄 끝 `(YYYY-MM-DDTHH:MMZ)` 에서 `now_at` 필드로 올라와요. Stop 게이트는 그 필드로 24시간을 봐요.
- `tier-from-state.sh --reset` 이 `current-task.json.lock` 을 잡아요 — 같은 파일을 쓰는 두 스크립트가 같은 락 문자열을 갖게.
reset 은 `handoff` 를 지우지 않아요 (next·open·renamed 는 task 를 넘어 살아야 해요).
- doctor 가 남은 `.ax/docs/STATUS.md` 를 마이그레이션 잔재로 알려요 (자동 삭제·import 없음 — 왜인지는 아래).
- `.ax/MEMORY.md` 는 그대로 둬요 — 고유 정보가 없는 재생성 캐시고 이미 gitignore 돼 있어요. 바뀐 건 인계 노트 포인터 한 줄.

## 행동 변화

- `status-note.sh` 변이 모드는 `current-task.json` 이 없으면 exit 1 로 거절해요 (`/up` 먼저). 최소 파일을 만들면 `->` seed 가 영원히 막혀요.
- 상한 40 은 이제 "항목 40개" 예요 (헤더·빈 줄이 없어져 실효 예산이 조금 늘어요).
- jq 가 없으면 `status-note.sh` 가 exit 2 로 건너뛰어요 — `zero-ablation.sh --on` 의 기한 체크박스 기록(`|| true`)도 조용히 같이 건너뛰어요. 예전 텍스트 모드는 awk 로 돌아서 jq 없이도 썼어요.
- `--show --json` 의 `lines` → `items`, `now_at` 필드가 새로 생겨요.
- 기존 STATUS.md 의 줄은 자동으로 옮기지 않아요. 필요한 줄만 `status-note.sh --add <절> "…"` 로 옮기고 파일을 지워요. doctor 가 남은 파일을 잔재로 알려요.

## 왜 이 자리인가

**`current-task.json` 에 두는 것의 정직한 비용.** (1) `current-task.json` 을 쓰는 코드가 하나 늘어 **같은 파일 동시 쓰기 노출**이 생겨요 — 스크립트 writer 둘은 같은 락으로 직렬화하지만 SKILL.md 인라인 jq 5곳은 무락이에요 (두 세션이 같은 리포를 동시에 쓸 때만 문제, 인라인 jq 끼리는 오늘도 같은 노출). (2) writer 가 객체를 통째로 재조립하면 `handoff` 가 조용히 사라져요 — 그래서 smoke 에 "in-place jq 만" 구조 가드를 계약으로 넣어요. (3) `current-task.json` 이 **task 를 넘어 사는 하위 객체**를 하나 갖게 돼요 — `reset-task.sh` 가 `handoff` 를 남기는 걸 문서(`CLAUDE.md` · `skill-routing.md` · `reset-task.sh` 헤더)와 smoke 로 고정해요. 이 셋을 감수하는 이유는 파일 수를 늘리지 않는 것이 이번 과제의 목적 그 자체이기 때문이에요.

**MEMORY.md 를 그대로 두는 이유.** STATUS.md 의 문제는 "고유 정보가 상태 파일이 아닌 곳에, gitignore 도 없이" 있었던 거예요. MEMORY.md 는 셋 다 해당하지 않아요 — 고유 정보가 없고, 재생성이고, gitignore 돼 있어요. `state.json.hud` 가 `update-state.sh` 의 렌더 캐시인 것과 같은 지위예요. 이번 결정에 필요한 변경은 **인계 노트 포인터 한 줄을 `handoff` 기준으로 바꾸는 것**뿐이라 그것만 해요.

## 검증

- smoke 571 통과 / 0 실패 (기준선 559 → +12). 재작성·추가한 검사: 인계 노트 형식 고정 · 동시 쓰기(`--init`·`--add`·`tier-from-state --reset`) 무손실 · dry-run 무락 · 선점 락 exit 1 유지 · writer in-place 가드 · doctor STATUS.md 잔재 통지.
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.5.11](0.5.11.md) — 인계 노트가 세 번째 상태 파일이던 것 (`.ax/docs/STATUS.md` → `current-task.json` 의 `handoff`)
- [0.5.10](0.5.10.md) — dry-run 이 spec 번호를 태우던 것 (예약은 쓰기인데 호출부가 --dry-run 을 안 넘김) + 중첩 `.suggested` gitignore 구멍
- [0.5.9](0.5.9.md) — I5 가 pre-commit hook 을 늘 미등록으로 세던 것 (glob 디스패처를 안 봐서 — 문서는 처음부터 맞게 적혀 있었어요)
- [0.5.8](0.5.8.md) — 한글을 반으로 가르며 awk 를 죽이던 길이 상한, 그 실패가 ok 로 나가던 것 (`GOAX_AWK_CLIP` 낱말 경계 clip SSOT + build-memory 자기 점검)
Expand Down
5 changes: 4 additions & 1 deletion docs/skill-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,14 @@ idle → triaged → spec → spec_checked → tasks → implementing → review
| `review` | spec-implement §8.1 (evaluator 대기) | `review ●` |
| `idle` | spec-implement §8.2 (`reset-task.sh`) | `idle` |

`handoff` 하위 객체 (`current-task.json.handoff`) — 누가 쓰나: `status-note.sh` 만. 언제: halt·완료·레인 보고·zero §13·onboarding 9단계.
`reset-task.sh` 의 phase 리셋에 살아남아요 (next·open·renamed 는 task 를 넘어 살아야 해요).

`done`·`blocked` 는 폐기됐어요 — 완료는 `reset-task.sh` 가 곧바로 idle 로 닫고, 막힘은 `blocked_by` 배열이 표현해요.

각 skill이 phase 갱신. `doctor`가 phase 보고 결손 진단.

**세션 간 인계는 phase 가 아니라 `.ax/docs/STATUS.md`** 예요 (`status-note.sh` — 지금 상태 · 다음 · 열린 질문 · 이번에 바뀐 이름). `triage` 가 매 작업 진입 때 MEMORY.md 보다 먼저 읽고, `spec-implement` 가 halt·완료·레인 보고 시점에 갱신하고, `zero` 가 첫날 끝에 개설해요. 대화가 압축되면 사라지는 것만 담고, 끝난 항목은 지워요.
**세션 간 인계는 phase 가 아니라 `current-task.json` 의 `handoff`** 예요 (`status-note.sh` — 지금 상태 · 다음 · 열린 질문 · 이번에 바뀐 이름). `triage` 가 매 작업 진입 때 MEMORY.md 보다 먼저 읽고, `spec-implement` 가 halt·완료·레인 보고 시점에 갱신하고, `zero` 가 첫날 끝에 개설해요. 대화가 압축되면 사라지는 것만 담고, 끝난 항목은 지워요.

## Mistake Loop — capture(mistake) vs review(audit) 분리

Expand Down
Loading
Loading