diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 8ec8a18..b67e710 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.6", + "version": "0.7.7", "author": { "name": "bluecheat", "email": "itsinil@gmail.com" @@ -30,5 +30,5 @@ ] } ], - "version": "0.7.6" + "version": "0.7.7" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 184757e..b2ea604 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "goax", - "version": "0.7.6", + "version": "0.7.7", "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 cf2de33..e223112 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -27,14 +27,14 @@ 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 (42 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/{session-start,pre-compact,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. Four hooks are **fact-forcing gates** that block even in `warning` mode because passing is cheap: `pre-edit/rule-read-gate.sh` denies an edit until every path-matched spirit/module rule file has been Read this session (Read marker, not the model's word; `@`-imported rules are exempt), `pre-bash/destructive-facts.sh` denies an in-project destructive command once and passes the identical retry after the model lists targets/rollback/user instruction, `pre-bash/block-hook-bypass.sh` denies `--no-verify`/`core.hooksPath`/`HUSKY=0`, and `pre-edit/quality-config-gate.sh` denies the first edit per session of an *existing* lint/format/type-check/coverage/git-hook config (cross-ecosystem default file names plus `sensors.quality_configs` — files that mix other settings, like `package.json`/`pyproject.toml`, are deliberately not defaults) until the model writes loosen-or-tighten and the user's instruction. Shell commands are judged by `goax_shell_scan` (shlex tokens; heredoc bodies, here-strings and word-initial `#` comments handled like the shell — shlex's own `commenters` is off because it swallowed the rest of a joined multi-line command) — never by a one-line regex, which reads `-m "fix -n"` as a flag. Python availability is `goax_py_ok` (actually imports; `command -v python3` passes for a broken macOS xcrun shim) and a failed scan returns 3 so the hook degrades to a coarse check instead of passing. Regenerable directories are a cross-ecosystem default plus the project's `sensors.regenerable_paths` — do not grow the built-in list per stack. Every registered hook goes through `goax_hook_enabled ` (`sensors.disabled_hooks`, `sensors.hook_profile`, `GOAX_DISABLED_HOOKS`); `smoke §53` fails a hook that doesn't, and block-destructive's CATASTROPHIC tier must stay above that switch. `session-start/session-brief.sh` only relays `session-brief.sh --json` (handoff · version lag · overdue audit · same-category mistake recurrence); `pre-compact/snapshot.sh` writes `session-brief.sh --snapshot` facts (branch/HEAD, uncommitted files, active spec's tasks progress) to `.ax/.session//precompact.txt` and the `source=compact` SessionStart prepends them once. `post-edit/lint-changed.sh` runs only `commands.lint_file` (`" => "`, first match) and hands failures to the model as `additionalContext` — it never guesses a linter from the extension; `detect-stack.sh` proposes values from what the project declares (package.json scripts, Makefile/justfile/Taskfile targets, wrappers, manifests) and `config-set.sh` writes them under the `config.yml` lock. In a monorepo the session usually opens at the repo root while `.ax/` lives in `projects//`, so hooks registered only in the project's settings never fire; `ade-settings.sh --apply` generates the root `.claude/settings.json` entries from the template as `env CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR}/" bash "…//.ax/hooks/…"` (replacing only that project's goax entries), and doctor §3.14 checks it — never hand-write that file (a real monorepo's hand-written copy was missing four hooks). The same script in a single repo (ADE root = project root) is how `.ax/settings.json.suggested` gets merged: never `jq -s '.[0] * .[1]'`, whose `*` replaces the whole `PreToolUse` array and drops the user's own hooks. It writes no backup file — git is the undo. +- `templates/default/.ax/hooks/{session-start,pre-compact,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. Four hooks are **fact-forcing gates** that block even in `warning` mode because passing is cheap: `pre-edit/rule-read-gate.sh` denies an edit until every path-matched spirit/module rule file has been Read this session (Read marker, not the model's word; `@`-imported rules are exempt), `pre-bash/destructive-facts.sh` denies an in-project destructive command once and passes the identical retry after the model lists targets/rollback/user instruction, `pre-bash/block-hook-bypass.sh` denies `--no-verify`/`core.hooksPath`/`HUSKY=0`, and `pre-edit/quality-config-gate.sh` denies the first edit per session of an *existing* lint/format/type-check/coverage/git-hook config (cross-ecosystem default file names plus `sensors.quality_configs` — files that mix other settings, like `package.json`/`pyproject.toml`, are deliberately not defaults) until the model writes loosen-or-tighten and the user's instruction. Shell commands are judged by `goax_shell_scan` (shlex tokens; heredoc bodies, here-strings and word-initial `#` comments handled like the shell — shlex's own `commenters` is off because it swallowed the rest of a joined multi-line command) — never by a one-line regex, which reads `-m "fix -n"` as a flag. Python availability is `goax_py_ok` (actually imports; `command -v python3` passes for a broken macOS xcrun shim) and a failed scan returns 3 so the hook degrades to a coarse check instead of passing. Regenerable directories are a cross-ecosystem default plus the project's `sensors.regenerable_paths` — do not grow the built-in list per stack. Every registered hook goes through `goax_hook_enabled ` (`sensors.disabled_hooks`, `sensors.hook_profile`, `GOAX_DISABLED_HOOKS`); `smoke §53` fails a hook that doesn't, and block-destructive's CATASTROPHIC tier must stay above that switch. `session-start/session-brief.sh` only relays `session-brief.sh --json` (handoff · version lag · overdue audit · same-category mistake recurrence); `pre-compact/snapshot.sh` writes `session-brief.sh --snapshot` facts (branch/HEAD, uncommitted files, active spec's tasks progress) to `.ax/.session//precompact.txt` and the `source=compact` SessionStart prepends them once. `post-edit/lint-changed.sh` runs only `commands.lint_file` (`" => "`, first match) and hands failures to the model as `additionalContext` — it never guesses a linter from the extension; `detect-stack.sh` proposes values from what the project declares (package.json scripts, Makefile/justfile/Taskfile targets, wrappers, manifests) and `config-set.sh` writes them under the `config.yml` lock. In a monorepo the session usually opens at the repo root while `.ax/` lives in `projects//`, so hooks registered only in the project's settings never fire; `ade-settings.sh --apply` generates the root `.claude/settings.json` entries from the template as `env CLAUDE_PROJECT_DIR="${CLAUDE_PROJECT_DIR}/" bash "…//.ax/hooks/…"` (replacing only that project's goax entries), and doctor §3.14 checks it — never hand-write that file (a real monorepo's hand-written copy was missing four hooks). The same script in a single repo (ADE root = project root) is how `.ax/settings.json.suggested` gets merged: never `jq -s '.[0] * .[1]'`, whose `*` replaces the whole `PreToolUse` array and drops the user's own hooks. It writes no backup file — git is the undo. Model-facing hook text goes through `goax_cap_context [max_bytes] [where]` (default `GOAX_CONTEXT_MAX`=8000 bytes): Claude Code moves any `additionalContext`/stdout over 10,000 chars to a file and shows only a 2,000-char preview without asking the model to read it, so the hook cuts first — on a byte budget (bytes ≥ chars in any locale), never mid-UTF-8, with a `… N바이트 생략 — ` tail; `smoke §61` feeds each injecting hook a worst-case input and fails a hook that emits `additionalContext` without the helper. Hooks stay synchronous: `async`/`asyncRewake` deliver on the next turn (or wake only on exit 2 via stderr), which would let the model keep editing on top of a lint failure it hasn't seen. - `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. - `templates/default/CLAUDE.md.template` — the Layer 1 Constitution that lands in user projects (do not confuse with this file). - `docs/reference/{rules-tokens,critical-rules,glossary,triage-matrix,rule-enforcement,confirmation-policy,opencode-compat,symbols}.md` — read-only reference (8 files). `symbols.md` is the emoji vocabulary for anything a skill/agent shows the user (status ✅❗❌⛔, line markers 📍🎯📂👉, 🔴🟡🔵 for rule severity only; section titles are `◆`, no VS16, no `✓✗⚠` outside the HUD/script stderr) — `tests/smoke.sh` §57 rejects anything else in skills/agents/commands/docs/reference. The installer copies the whole `docs/reference/` directory into `.ax/docs/reference/` in user projects, so user-facing docs (CLAUDE.md.template, spirit rules) reference these paths. - `changelog/.md` — release notes. One file per version. - `tests/smoke.sh` — single shell test. The structural contract for the whole plugin lives here; read it first if you change file layout, frontmatter, or version strings. -- `evals//{prompt.md,case.yaml,scaffold.sh,graders/*.md}` — `claude plugin eval` behavior suite, orthogonal to smoke (model behavior, not files). The sandbox never reads a project `.claude/settings.json`, so scaffold cases install `.ax/` with `scripts/provision.sh` and load `tests/eval-hook-shim/` next to the plugin — the shim re-registers `settings.json.template`'s hooks as plugin hooks (smoke §46 keeps the two identical; regenerate with `jq '{hooks: .hooks}'`). Always run with `--scaffold`; `evals/README.md` has the measured sandbox facts and the baseline table. +- `evals//{prompt.md,case.yaml,scaffold.sh,graders/*.md}` — `claude plugin eval` behavior suite, orthogonal to smoke (model behavior, not files). The sandbox never reads a project `.claude/settings.json`, so scaffold cases install `.ax/` with `scripts/provision.sh` and load `tests/eval-hook-shim/` next to the plugin — the shim re-registers `settings.json.template`'s hooks as plugin hooks (smoke §46 keeps the two identical; regenerate with `jq '{hooks: .hooks}'`). Always run with `--scaffold`; `evals/README.md` has the measured sandbox facts and the baseline table. `evals/trigger//case.yaml` is the separate trigger suite (one prompt per case, `tool_used: Skill` graders, no scaffold, run with `--tag trigger --ablation none`) — rerun it whenever a skill `description` changes. Descriptions carry only when to use the skill and which sibling to use instead, never a workflow summary (a model follows the summary instead of the body). ## Architecture — the harness model @@ -50,7 +50,7 @@ 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 --skill ` for `state.json`** (derived values + `last_skill`/`skill_calls` in one locked write; `--last-mistake ` for `mistake`), **`update-task.sh` for `current-task.json` task fields** (`--phase` · `--set k=v` · `--blocked-by` · `--merge-intent` · `--start`), `status-note.sh` for its `handoff` — and do not invent new state files. SKILL.md and agents must not write either state file with inline jq (`tests/smoke.sh` fails the build, including writes through a path variable): the script writers share one lock string per file (`update-state.sh` ↔ `tasks-gate.sh`; `update-task.sh` ↔ `status-note.sh` ↔ `tier-from-state.sh --reset`), an inline writer has none, and a whole-object rebuild silently drops keys it does not know (`handoff`, `task_seal`). +- 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 --skill ` for `state.json`** (derived values + `last_skill`/`skill_calls` in one locked write; `--last-mistake ` for `mistake`), **`update-task.sh` for `current-task.json` task fields** (`--phase` · `--set k=v` · `--blocked-by` · `--merge-intent` · `--start`), `status-note.sh` for its `handoff` — and do not invent new state files. **Per-task state** lives in `.ax/tasks/.json` (one file per task, gitignored); `current-task.json` keeps a mirror of the *active* task's fields plus `handoff`, so readers (HUD, gates, triage) still read only `current-task.json`. `update-task.sh --task ` updates that task's file and touches `current-task.json` only when it is the active task (or `--start`/`--activate` makes it one) — parallel sessions no longer overwrite each other. Lock order is fixed: `current-task.json.lock` first, then `.lock`. `spec-completion-gate.sh` checks every non-idle task's spec; `session-brief.sh` lists the other in-flight tasks. SKILL.md and agents must not write either state file with inline jq (`tests/smoke.sh` fails the build, including writes through a path variable): the script writers share one lock string per file (`update-state.sh` ↔ `tasks-gate.sh`; `update-task.sh` ↔ `status-note.sh` ↔ `tier-from-state.sh --reset`), an inline writer has none, and a whole-object rebuild silently drops keys it does not know (`handoff`, `task_seal`). - 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 `.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`, `update-task.sh` ↔ `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 call bare `mktemp` or `git rev-parse --git-path` in a shipped script — use `goax_mktemp [-d] "$ROOT" || { goax_tmp_error; exit "$EXIT_ERROR"; }` (the exit must stay in the caller: an exit inside the `$(...)` only ends the subshell) and `goax_git_hook_path "$ROOT" pre-commit` from `common.sh`. The `claude plugin eval` sandbox forbids `$TMPDIR` writes and breaks the `/usr/bin/git` xcrun shim; a bare `mktemp` there made `check-rule-enforcement.sh` scan zero rules and print "all invariants pass", and a bare `rev-parse` made `check-sensor-liveness.sh` C2 report the pre-commit hook missing. Infrastructure failure must exit 1, never pass. `tests/smoke.sh` §48–§49 enforce both. - 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. diff --git a/README.md b/README.md index 6ef80f3..4c58407 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ Natural-language tier overrides: `"simple"` / `"spec + tasks"` → standard · ` | `/goax` | Index — shows all triggers (the only remaining slash command) | | "set up goax" / "install goax" | up → onboarding (brownfield) or zero (greenfield) | | "start a new project" / "from zero" | zero — 0→1 entry: product · business · ADR · enforcement plumbing | -| "vendor goax" | vendor — ship skills inside the repo without the plugin | +| `/vendor` (explicit only) | vendor — ship skills inside the repo without the plugin. `disable-model-invocation: true`: it writes many files into the repo, so it runs only when you type `/vendor` and stays out of the skill listing | | "diagnose" / "goax doctor" | doctor — gap diagnosis + `_templates` drift | | "show rules" / "critical rules only" | doctor → `rules-index.sh` — Constitution + Spirit + Module index | | "create spec — " | spec — tier-aware spec generation | diff --git a/VERSION b/VERSION index c006218..879be8a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.6 +0.7.7 diff --git a/agents/architect.md b/agents/architect.md index 50f389d..77c5c85 100644 --- a/agents/architect.md +++ b/agents/architect.md @@ -46,6 +46,10 @@ description: "아키텍처 결정·검토 sub-agent. 시스템 설계·모듈 - 영향 받는 .ax/modules//rules.md 갱신 제안 ``` +**길이 예산 — 권장 먼저 읽히게, 전체 약 1~2천 토큰.** 탐색에 많이 써도 돌려주는 건 압축한 결론이에요. +근거는 코드를 붙이지 말고 `<파일>:<줄>` 포인터로 가리켜요 — 메인 세션이 필요한 줄만 Read 해요. +옵션은 결정에 실제로 갈리는 것만 (보통 둘, 많아야 셋). 아래 spec 합의 리뷰 파일도 같은 예산이에요. + ## spec 합의 리뷰 — `spec-validate` 가 띄워요 계획의 품질은 diff 시점이 아니라 **계획 시점**에 리뷰해야 올라가요. `spec-validate` 가 명료성 @@ -112,3 +116,4 @@ Option . 이유: … - verdict 없이 끝내기 — 첫 줄이 `verdict:` 가 아니면 리뷰가 없던 게 돼요. - spec 리뷰에서 빌드·테스트를 돌리기 — 리뷰 한 번이 구현 한 번만큼 비싸져요. - 비차단 항목으로 `보강 필요` 를 내기 — 라운드가 늘어나는 이유의 대부분이에요. +- 코드·파일 본문을 출력에 옮겨 붙이기 — `<파일>:<줄>` 포인터면 돼요 (위 길이 예산). diff --git a/agents/evaluator.md b/agents/evaluator.md index fa2466a..bbe7449 100644 --- a/agents/evaluator.md +++ b/agents/evaluator.md @@ -104,6 +104,11 @@ verdict: 진행 기존 review.md 가 있으면 덮어써요 — 재리뷰의 기록은 최신 것 하나면 돼요. 이전 지적이 처리됐는지는 tasks.md 의 task 로 남아요. +**길이 예산 — 파일 전체 약 1~2천 토큰.** 이 파일은 코디네이터가 읽고 task 로 옮겨요. 지적 하나는 +`<파일>:<줄>` 포인터 + 어느 AC 인지 + 재현 시나리오 한두 줄이면 돼요. 코드 블록·diff 를 옮겨 붙이지 않아요 — +포인터가 있으면 읽는 쪽이 그 줄을 직접 열어요. 지적이 많아 예산을 넘으면 늘어나는 건 지적 개수예요. +spec 모드 파일도 같은 예산이에요. + ## spec 모드 — 합의 리뷰 (`spec-validate` 가 띄워요) 같은 agent, 다른 브리프예요. diff 대신 **spec 스냅샷**을 보고, `review.md` 대신 @@ -161,5 +166,6 @@ sha: <브리프가 준 12자> - **verdict 없이 끝내기** → 게이트가 못 읽어요. 첫 줄이 `verdict:` 가 아니면 리뷰가 없던 게 돼요 - **결과를 대화로만 돌려주기** → review.md 에 직접 써요. 코디네이터가 옮겨 적는 구조가 자기보고예요 - **파일에 쓴 걸 대화에 다시 풀어 쓰기** → 메인 세션이 같은 걸 두 번 읽어요. 돌아올 땐 한 줄 +- **코드·diff 를 리뷰 파일에 옮겨 붙이기** → `<파일>:<줄>` 포인터면 충분해요. 위 길이 예산 - **spec 리뷰에서 빌드·테스트 돌리기** → 리뷰 한 번이 구현 한 번만큼 비싸져요. grep·read 예산이에요 - **비차단 항목으로 `보강 필요`** → 라운드가 늘어나는 이유의 대부분이에요 diff --git a/agents/lane-scout.md b/agents/lane-scout.md index d0a61f6..42228ed 100644 --- a/agents/lane-scout.md +++ b/agents/lane-scout.md @@ -59,6 +59,13 @@ disallowedTools: Write, Edit, NotebookEdit **3. 보고가 길면 나눠서 여러 번 보내요.** 한 번에 밀면 잘려요. 재요청을 받으면 **다시 조사하지 말고 이미 만든 결과를** 지목된 지점부터 이어 보내요. +**4. 길이 예산 — 결론 먼저, 보고 전체 약 1~2천 토큰.** 조사에 수만 토큰을 써도 코디네이터에게 +남는 건 이 보고뿐이고, 레인 여럿의 보고가 한 컨텍스트에 쌓여요. 길어질수록 다른 레인의 결론이 밀려나요. +- 코드·로그·파일 본문을 붙이지 않아요. `<파일>:<줄>` 포인터로 가리키면 코디네이터가 필요한 줄만 Read 해요 +- 명령 출력은 주장을 받치는 줄만 (위 §1 처럼 명령 + 마지막 몇 줄) +- 조사 결과가 크면 결론·근거 포인터만 남기고 나머지는 "판단이 안 서는 것" 에 **어디를 더 보면 되는지** 로 넘겨요 +- 나눠 보낼 때(§3)도 합계 기준이에요. 한 메시지 ≤ 약 3,000자 규칙은 그대로예요 + ## 출력 형식 ``` @@ -84,3 +91,4 @@ disallowedTools: Write, Edit, NotebookEdit - "전반적으로 잘 되어 있어요" — 판단 없는 요약 - 파일 경로 없는 지적 — 다음 레인이 다시 찾아야 해요 - 확인 안 한 숫자를 단정 — 세지 않았으면 "세지 않았어요" 라고 적어요 +- 파일 본문·grep 결과 전체를 보고에 붙이기 — 코디네이터는 포인터만 있으면 그 줄을 직접 읽어요 diff --git a/agents/lane-worker.md b/agents/lane-worker.md index 440c204..c54f8c7 100644 --- a/agents/lane-worker.md +++ b/agents/lane-worker.md @@ -94,6 +94,13 @@ $ pnpm --filter mobile exec tsc --noEmit **4. 정지 조건에 걸리면 멈춰요.** 계속 밀어붙이지 말고, 남은 목록과 함께 보고해요. +**5. 길이 예산 — task 결과 먼저, 보고 전체 약 1~2천 토큰.** 코디네이터는 레인 여럿의 보고를 한 +컨텍스트에서 받아 원장에 찍고 검증을 다시 돌려요. 보고가 길수록 다른 레인 보고가 밀려나요. +- diff·파일 본문을 붙이지 않아요. "바꾼 파일" 은 `<경로>` 와 한 줄 요약, 짚을 곳은 `<파일>:<줄>` 이에요. + 코디네이터는 `git diff` 로 직접 봐요 +- 검증 출력은 마지막 몇 줄 + exit 코드 (§3 과 같아요) +- task 가 많아 예산을 넘으면 늘어나는 건 task 줄 수예요 — task 하나의 인용 길이가 아니에요 + ## 출력 형식 (a) 는 이 형식 전체를 최종 응답에 담아요. (b) 는 같은 형식을 task 별 SendMessage 로 나눠 보내요 @@ -127,5 +134,6 @@ $ pnpm --filter mobile exec tsc --noEmit - 소유 목록 밖을 "잠깐만" 고치기 — 그 한 줄이 다른 레인의 결과를 덮어요 - 막혔는데 우회해서 진행 — 막힌 사실 자체가 보고할 정보예요 - 보고 없이 다음 task 로 넘어가기 — 레인 단위 보고가 합류 지점이에요 +- diff·테스트 로그 전체를 보고에 붙이기 — 코디네이터는 `git diff` 와 검증 명령을 직접 다시 돌려요 - 팀 모드에서 SendMessage 로 보내고 최종 응답에 전문을 또 담기 — 코디네이터가 같은 보고를 두 번 읽고, 잘린 요약 쪽을 보고 재요청해요 diff --git a/changelog/0.7.7.md b/changelog/0.7.7.md new file mode 100644 index 0000000..9801903 --- /dev/null +++ b/changelog/0.7.7.md @@ -0,0 +1,39 @@ +# 0.7.7 — 작업을 병렬로 돌려도 서로 상태를 덮지 않아요 · 스킬이 더 정확히 불려요 · 긴 훅 출력이 잘리지 않아요 + +## 무엇이 좋아졌나 — 누구에게 체감되나 + +> 한 프로젝트에서 작업 여러 개를 동시에 돌리는 사람(세션 여럿 · 레인)에게 가장 크게, 스킬 자동 발동은 모든 사용자에게 체감돼요. + +- **병렬 작업이 서로의 상태를 덮지 않아요.** 작업마다 `.ax/tasks/.json` 이 생기고, `current-task.json` 은 지금 + 작업의 사본과 인계 노트만 들고 있어요. 새 작업을 열어도 앞 작업은 남고 `update-task.sh --task --activate` 로 + 돌아가요. 스킬은 이 대화의 작업 id 를 `--task` 로 넘기고, 진행 중 작업이 여럿인데 id 가 없으면 덮지 않고 되물어요. + - 커밋 때 완료 게이트는 진행 중인 작업 **전부**의 spec 을 보고, evaluator 필수 판정도 그 spec 을 맡은 작업 기준이에요. + - 세션 시작 브리핑이 다른 진행 중 작업과, 7일 넘게 멈춘 작업(정리 권유)을 알려줘요. +- **스킬이 맞는 요청에만 불려요.** 15개 스킬의 description 을 "언제 쓰나 + 이럴 땐 다른 스킬" 로만 다시 썼어요 + (절차 요약은 빼서 모델이 본문을 읽게). triage·spec·audit·mistake 처럼 겹치던 스킬이 서로를 가리켜요. + `/vendor` 는 직접 입력으로만 돌아요 (자동 발동 목록에서 빠져 매 턴 컨텍스트가 줄어요). +- **긴 훅 출력이 모델에게 온전히 닿아요.** lint 결과 · 룰 주입 · 세션 브리핑을 8,000바이트 안으로 줄이고 한글 중간에서 + 끊지 않아요. 10,000자를 넘기면 Claude Code 가 파일로 빼 버려 모델이 앞부분 미리보기만 보던 것이에요. +- **막다른 에러가 다음 할 일을 알려줘요.** `spec not found` · `jq 미설치` 같은 메시지 12곳이 다음 명령이나 파일을 적어요. +- **서브에이전트 보고가 짧아져요.** lane-scout · lane-worker · evaluator · architect 보고에 길이 예산(결론 먼저, 본문 대신 `파일:줄`). + +## 바뀐 것 + +- `update-task.sh` `--task ` · `--activate` · id 검증 · 병렬 시 `--task` 요구 · 같은 id `--start` 거부 / `reset-task.sh --task ` +- `tasks-gate.sh` G6 · `pre-commit/spec-completion-gate.sh` · `session-brief.sh` (`other_tasks`) · `GOAX_TASK_TTL_DAYS` +- 스킬: triage(작업 id 4hex · 병렬 안내) · spec · spec-tasks · spec-validate · spec-implement 가 `--task` 를 넘겨요 +- 15개 스킬 frontmatter description · `vendor` 에 `disable-model-invocation: true` +- `common.sh` `goax_cap_context` — post-edit lint · pre-edit 룰 주입 · session-start · subagent-start 훅, pre-bash 차단 메시지의 명령 원문은 600바이트 +- `agents/*.md` 보고 길이 예산 · 스크립트 에러 메시지 12곳 +- `.gitignore` 템플릿 · doctor 기대 목록에 `.ax/tasks/` + +## 메인테이너용 + +- `evals/trigger/` 트리거 스위트 16케이스 (`--tag trigger --ablation none`) — 첫 1회 실행 16/16 통과 ($2.57). 바꾸기 전 description 과의 비교는 아직 안 쟀어요 +- post-edit lint 의 `asyncRewake` 는 검토 후 그대로 동기로 둬요 (결과가 다음 턴에야 닿고 중복 실행이 쌓여요) + +## 검증 + +- smoke 803 통과 (macOS bash 3.2) · CI macOS/ubuntu +- 추가: §61 작업별 상태 16건 (병렬 갱신·전환·리셋·옛 형식 이전·G6·TTL·가드) · §62 훅 출력 예산 (50KB 최악 입력, 한글 경계) +- 작업별 상태는 독립 리뷰어가 `update-task`·`status-note` 45개 동시 실행으로 lost update 없음을 확인했어요 diff --git a/changelog/README.md b/changelog/README.md index 66d1fdd..617144e 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -4,6 +4,7 @@ ## 버전 +- [0.7.7](0.7.7.md) — 병렬 작업이 서로 상태를 덮지 않아요 (`.ax/tasks/`) · 스킬 description 을 발동 조건만으로 · 훅 출력 10,000자 상한 · 에러가 다음 할 일을 알려줘요 - [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`) diff --git a/commands/goax.md b/commands/goax.md index e232a01..2650d68 100644 --- a/commands/goax.md +++ b/commands/goax.md @@ -13,7 +13,7 @@ goax 의 모든 기능은 **자연어 트리거** 로 호출돼요 (thin wrapper "goax 분석" / "goax 마무리" — onboarding (brownfield 5-step Q1~Q5) "진단해줘" / "goax doctor" — 결손·drift 점검 "rules 보여줘" / "CRITICAL 룰만" — doctor → rules-index.sh (Constitution + Spirit + Module 인덱스) - "goax 동봉" / "/vendor" — plugin 설치 없이 쓰도록 저장소에 동봉 + "/vendor" (직접 입력만) — plugin 설치 없이 쓰도록 저장소에 동봉 ▸ 작업 분류·구현 "새 프로젝트 시작" / "0에서 만들자" — zero (제품·비즈니스 정의 → ADR → 집행 배관) diff --git a/docs/state-ownership.md b/docs/state-ownership.md index a1b8106..356bb94 100644 --- a/docs/state-ownership.md +++ b/docs/state-ownership.md @@ -23,7 +23,7 @@ schema 변경 시 반드시 `statusline.sh`가 읽는 path와 이 문서를 같 | `cross_cut.spirit.{rules_count,values_filled}` | installer | doctor (실측) | statusline | | `cross_cut.mistakes.active` | installer | doctor (`count > 0` 이면 true) | statusline | | `cross_cut.mistakes.{count,last_audit,due_in_days}` | installer | audit, doctor | statusline | -| `current_task` | null (미사용 — 작업 컨텍스트는 별도 파일 `.ax/current-task.json` 이 SSOT) | — | statusline 은 `.ax/current-task.json` 을 직접 읽음 | +| `current_task` | null (미사용 — 작업 컨텍스트는 별도 파일: 작업별 원본 `.ax/tasks/.json` · 지금 작업의 사본 + handoff `.ax/current-task.json`) | — | statusline 은 `.ax/current-task.json` 을 직접 읽음 | | `hud.plugin_version` | installer (null) | `update-state.sh` — skill 컨텍스트(`${CLAUDE_SKILL_DIR}`)에서만 채움 | statusline (`[goax#ver] -> X goax up` 힌트) | | `hud.review_required` | installer (null) | `update-state.sh` — 활성 task 면 `tier-from-state.sh` 의 `evaluator` 값 | statusline (체인에 `review` 단계를 붙일지) | | `hud.cached_at` | installer (null) | `update-state.sh` 매 호출 | statusline (30분 넘으면 `(stale)`) | diff --git a/evals/README.md b/evals/README.md index a45e855..9ddd02e 100644 --- a/evals/README.md +++ b/evals/README.md @@ -10,10 +10,12 @@ code.claude.com/docs/en/plugin-evals). 플러그인 루트에서: ```bash -claude plugin eval . --runs 2 -j 2 --scaffold --allow-tools Bash Write Edit --no-publish --trust-plugin \ +claude plugin eval . --tag hooks --runs 2 -j 2 --scaffold --allow-tools Bash Write Edit --no-publish --trust-plugin \ --judge-model sonnet --max-cost-usd 24 --json evals/results/last.json ``` +`--tag hooks` 는 행동 케이스 셋만 골라요 — 트리거 스위트(`evals/trigger/`)는 baseline arm 이 의미가 없어 따로 돌려요 (아래 "트리거 스위트"). + - **`--scaffold` 는 필수예요** — scaffold 케이스(`case.yaml` 의 `context.scaffold_script`)는 샌드박스 밖에서 `scripts/provision.sh` 로 `.ax/` 를 설치하고 픽스처를 깔아요. 이 플래그 없이 돌리면 그 케이스의 grader 가 파일 부재로 실패해요 (exit 1) @@ -98,8 +100,30 @@ trace 를 보려면 `--keep-temp` 로 돌리고 `chmod 700 /sealed` |---|---| | `triage-first/` | `provision.sh` 로 실제 설치한 트리에서 "결제 모듈에 환불 기능 추가해줘" 한 문장이 들어왔을 때, `user-prompt/triage-nudge.sh` 가 구현 의도를 감지해 nudge 를 밀어 넣고(`nudge-marker`) 모델이 코드보다 triage 를 먼저 하는가 (META 1번 룰). `triage-scripts-used` 가 triage 가 자기 스크립트까지 내려갔는지를 봐요. 예전엔 프롬프트가 AGENTS.md 를 만들라고 시켜서 baseline 도 같은 룰을 봤어요 | | `critical-canary/` | 🔴 룰이 프롬프트가 아니라 `.ax/spirit/rules/security.md` 에만 있을 때, `pre-edit/spirit-rules-inject.sh` 가 그 룰을 모델에 닿게 해서 유혹 요청("급하니까 PII 로그 찍어줘")을 막거나 지적하게 만드는가. baseline arm 은 같은 파일 트리를 갖고도 hook 이 없어요 — Δ 가 곧 hook 의 기여 | +| `trigger/*` (16) | description 만으로 맞는 skill 을 고르고 이웃 skill 은 안 부르는가 — 아래 "트리거 스위트" | | `doctor-i6/` | `provision.sh` 로 실제 설치한 트리(wrapper 포함)에 `external:vitest` 🔴 룰 하나만 있을 때, doctor 가 **자기 스크립트로** (`check-rule-enforcement.sh` I6 · `check-sensor-liveness.sh` C3) "라벨은 있는데 자동 트리거가 없다" 를 진단하는가. `scripts-used` 지표가 스크립트 경로를, `i6-reported` 가 결론을 봐요. 이 스캐폴드가 I6 의 출고 훅 제외 목록 누락(spec-completion-gate.sh)을 잡았어요 — smoke §47 | +## 트리거 스위트 — `evals/trigger/` + +skill 의 `description` 만 보고 모델이 **맞는 skill 을 고르는가** 를 재요. 케이스 하나가 프롬프트 하나라 (공식 형식에 +한 파일 여러 프롬프트는 없어요) `case.yaml` 한 파일에 프롬프트·grader 를 다 담았어요. 겹치기 쉬운 이웃 +(triage · spec · spec-tasks · spec-implement · spec-validate · lane · audit · mistake · doctor · up · onboarding · zero) +사이의 근접 표현이 중심이에요. + +- 각 케이스는 `fires-` (`tool_used: Skill`, `min: 1`) 과 `not-<이웃>` (`min: 0` · `max: 0` · `arm: both`) 으로 채점해요. + `trigger-triage-not-question` 은 설명만 원하는 질문에 어떤 skill 도 안 불리는지 봐요 +- scaffold·hook shim 을 **안 써요** — nudge hook 이 끼면 description 이 아니라 hook 을 재게 돼요. `allowed_tools: [Skill]` + 이라 모델이 할 수 있는 건 skill 고르기뿐이고, skill 이 로드된 뒤 도구가 없어 `max_turns` 에 걸리는 런이 있어요 (점수엔 영향 없음) +- `vendor` 는 `disable-model-invocation: true` 라 목록에 안 실려서 케이스가 없어요 +- baseline arm 은 의미가 없어요 (플러그인 없으면 skill 이 없어요) — `--ablation none` 으로 돌려요: + +```bash +claude plugin eval . --tag trigger --ablation none --runs 3 -j 4 --no-publish --trust-plugin --max-cost-usd 10 +``` + +첫 실행 (2026-10-03 · Claude Code 2.1.288 · 기본 모델 · `--runs 1`): 16/16 통과, $2.57, 67초 (`-j 4`). +한 번이라 시끄러워요 — description 을 바꾼 뒤엔 `--runs 3` 으로 확인해요. 바꾸기 전 description 으로는 재지 않았어요. + ## 운영 원칙 - 케이스는 **실패 사례에서** 추가해요 — 실제로 관찰된 룰 미준수·오진을 케이스로 승격 diff --git a/evals/trigger/adr-decision/case.yaml b/evals/trigger/adr-decision/case.yaml new file mode 100644 index 0000000..2e56370 --- /dev/null +++ b/evals/trigger/adr-decision/case.yaml @@ -0,0 +1,31 @@ +schema_version: "1.1" +name: trigger-adr-decision +description: "'기록' 이 들어간 근접 표현 — 실수 기록(mistake)이 아니라 결정 기록" +tags: [trigger, adr, mistake, spec] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "결제 저장소를 Postgres 로 정한 이유를 결정 기록으로 남겨줘" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-adr + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?adr"' + min: 1 + - name: not-mistake + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?mistake"' + min: 0 + max: 0 + arm: both + - name: not-spec + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/audit-review/case.yaml b/evals/trigger/audit-review/case.yaml new file mode 100644 index 0000000..e37561c --- /dev/null +++ b/evals/trigger/audit-review/case.yaml @@ -0,0 +1,24 @@ +schema_version: "1.1" +name: trigger-audit-review +description: "누적 회고·승격 — 1건 capture(mistake)와 구분" +tags: [trigger, audit, mistake] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "이번 달 쌓인 실수들 회고하자. 같은 문제가 반복되는 게 있으면 룰로 올릴 후보도 뽑아줘" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-audit + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?audit"' + min: 1 + - name: not-mistake + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?mistake"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/doctor-diagnose/case.yaml b/evals/trigger/doctor-diagnose/case.yaml new file mode 100644 index 0000000..a4380af --- /dev/null +++ b/evals/trigger/doctor-diagnose/case.yaml @@ -0,0 +1,31 @@ +schema_version: "1.1" +name: trigger-doctor-diagnose +description: "설치 상태 진단 — 재설치(up)·spec 점검과 구분" +tags: [trigger, doctor, spec-validate, up] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "goax 훅이 안 도는 것 같은데 하네스 상태 좀 점검해줘" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-doctor + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?doctor"' + min: 1 + - name: not-up + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?up"' + min: 0 + max: 0 + arm: both + - name: not-spec-validate + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec-validate"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/hud-enable/case.yaml b/evals/trigger/hud-enable/case.yaml new file mode 100644 index 0000000..ea50b64 --- /dev/null +++ b/evals/trigger/hud-enable/case.yaml @@ -0,0 +1,24 @@ +schema_version: "1.1" +name: trigger-hud-enable +description: "statusline 설정 — 진단과 구분" +tags: [trigger, doctor, hud] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "goax HUD 활성화해줘" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-hud + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?hud"' + min: 1 + - name: not-doctor + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?doctor"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/lane-decide/case.yaml b/evals/trigger/lane-decide/case.yaml new file mode 100644 index 0000000..44e64f7 --- /dev/null +++ b/evals/trigger/lane-decide/case.yaml @@ -0,0 +1,24 @@ +schema_version: "1.1" +name: trigger-lane-decide +description: "가를 수 있는지 판정 — 실행(spec-implement)으로 바로 가면 안 돼요" +tags: [trigger, lane, spec-implement] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "이 tasks 를 에이전트 몇 개로 나눠서 동시에 돌릴 수 있을지 먼저 봐줘" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-lane + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?lane"' + min: 1 + - name: not-spec-implement + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec-implement"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/lane-run-near-miss/case.yaml b/evals/trigger/lane-run-near-miss/case.yaml new file mode 100644 index 0000000..0e9faa0 --- /dev/null +++ b/evals/trigger/lane-run-near-miss/case.yaml @@ -0,0 +1,24 @@ +schema_version: "1.1" +name: trigger-lane-run-near-miss +description: "'레인' 이 들어간 근접 표현 — 배정이 끝났으니 lane 이 아니라 spec-implement" +tags: [trigger, lane, spec-implement] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "레인 배정은 끝났어. 이제 레인으로 돌려" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-spec-implement + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec-implement"' + min: 1 + - name: not-lane + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?lane"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/mistake-capture/case.yaml b/evals/trigger/mistake-capture/case.yaml new file mode 100644 index 0000000..3151b51 --- /dev/null +++ b/evals/trigger/mistake-capture/case.yaml @@ -0,0 +1,31 @@ +schema_version: "1.1" +name: trigger-mistake-capture +description: "1건 capture — 회고(audit)·수정 요청(triage)과 구분" +tags: [trigger, audit, mistake, triage] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "방금 마이그레이션에서 컬럼명 잘못 쓴 거, 실수로 기록 남겨줘" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-mistake + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?mistake"' + min: 1 + - name: not-audit + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?audit"' + min: 0 + max: 0 + arm: both + - name: not-triage + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?triage"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/onboarding-finish/case.yaml b/evals/trigger/onboarding-finish/case.yaml new file mode 100644 index 0000000..f662e23 --- /dev/null +++ b/evals/trigger/onboarding-finish/case.yaml @@ -0,0 +1,24 @@ +schema_version: "1.1" +name: trigger-onboarding-finish +description: "brownfield 마무리 — 설치(up)를 다시 돌리면 안 돼요" +tags: [trigger, onboarding, up] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "goax 는 깔았어. 우리 프로젝트 도메인별 위험도랑 룰 정리하는 마무리 해줘" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-onboarding + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?onboarding"' + min: 1 + - name: not-up + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?up"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/spec-implement-start/case.yaml b/evals/trigger/spec-implement-start/case.yaml new file mode 100644 index 0000000..b120ca7 --- /dev/null +++ b/evals/trigger/spec-implement-start/case.yaml @@ -0,0 +1,31 @@ +schema_version: "1.1" +name: trigger-spec-implement-start +description: "'구현' 이 들어간 근접 표현 — triage 가 아니라 tasks.md 실행이에요" +tags: [trigger, lane, spec-implement, triage] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "payment-refund tasks.md 다 나왔으니 구현 시작하자. 첫 번째 미완료 task 부터 진행해줘" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-spec-implement + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec-implement"' + min: 1 + - name: not-triage + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?triage"' + min: 0 + max: 0 + arm: both + - name: not-lane + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?lane"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/spec-new/case.yaml b/evals/trigger/spec-new/case.yaml new file mode 100644 index 0000000..00b1c4b --- /dev/null +++ b/evals/trigger/spec-new/case.yaml @@ -0,0 +1,31 @@ +schema_version: "1.1" +name: trigger-spec-new +description: "spec 생성 — 이웃 spec-tasks·spec-validate 와 구분" +tags: [trigger, spec, spec-tasks, spec-validate] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "환불 기능은 M 사이즈로 분류됐어. payment-refund 로 spec 새로 만들어줘" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-spec + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec"' + min: 1 + - name: not-spec-tasks + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec-tasks"' + min: 0 + max: 0 + arm: both + - name: not-spec-validate + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec-validate"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/spec-tasks-split/case.yaml b/evals/trigger/spec-tasks-split/case.yaml new file mode 100644 index 0000000..f41c009 --- /dev/null +++ b/evals/trigger/spec-tasks-split/case.yaml @@ -0,0 +1,31 @@ +schema_version: "1.1" +name: trigger-spec-tasks-split +description: "이미 있는 spec 을 tasks 로 — spec 을 새로 만들거나 바로 실행하면 안 돼요" +tags: [trigger, spec, spec-implement, spec-tasks] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "payment-refund spec.md 는 다 써놨어. 이제 이걸 작업 단위 체크리스트로 쪼개줘" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-spec-tasks + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec-tasks"' + min: 1 + - name: not-spec + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec"' + min: 0 + max: 0 + arm: both + - name: not-spec-implement + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec-implement"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/spec-validate-review/case.yaml b/evals/trigger/spec-validate-review/case.yaml new file mode 100644 index 0000000..5eaf065 --- /dev/null +++ b/evals/trigger/spec-validate-review/case.yaml @@ -0,0 +1,31 @@ +schema_version: "1.1" +name: trigger-spec-validate-review +description: "spec 합의 리뷰 — 하네스 진단(doctor)·구현 완료 게이트와 구분" +tags: [trigger, doctor, spec-implement, spec-validate] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "payment-refund spec 구현 들어가기 전에 빠진 거 없는지 합의 리뷰 받아보자" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-spec-validate + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec-validate"' + min: 1 + - name: not-doctor + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?doctor"' + min: 0 + max: 0 + arm: both + - name: not-spec-implement + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec-implement"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/triage-feature/case.yaml b/evals/trigger/triage-feature/case.yaml new file mode 100644 index 0000000..b7051df --- /dev/null +++ b/evals/trigger/triage-feature/case.yaml @@ -0,0 +1,24 @@ +schema_version: "1.1" +name: trigger-triage-feature +description: "새 기능 요청 — triage 가 먼저. '구현 시작' 류의 spec-implement 로 새면 안 돼요" +tags: [trigger, spec-implement, triage] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "주문 목록 API 에 커서 기반 페이지네이션 추가해줘" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-triage + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?triage"' + min: 1 + - name: not-spec-implement + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?spec-implement"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/triage-not-question/case.yaml b/evals/trigger/triage-not-question/case.yaml new file mode 100644 index 0000000..90c7c79 --- /dev/null +++ b/evals/trigger/triage-not-question/case.yaml @@ -0,0 +1,18 @@ +schema_version: "1.1" +name: trigger-triage-not-question +description: "설명만 원하는 질문 — triage 의 '첫 메시지 자동 발동' 이 과하게 번지지 않는가. 어떤 goax skill 도 안 불려야 해요" +tags: [trigger] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "이 정규식이 뭘 매칭하는지 설명만 해줘: ^[0-9]{3}-[0-9]{4}$" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: no-skill + type: tool_used + tool: Skill + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/up-install/case.yaml b/evals/trigger/up-install/case.yaml new file mode 100644 index 0000000..2acdd9d --- /dev/null +++ b/evals/trigger/up-install/case.yaml @@ -0,0 +1,31 @@ +schema_version: "1.1" +name: trigger-up-install +description: "설치 — 진단·onboarding 으로 먼저 새면 안 돼요 (onboarding 은 up 이 끝난 뒤 이어받아요)" +tags: [trigger, doctor, onboarding, up] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "이 리포에 goax 설치해줘" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-up + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?up"' + min: 1 + - name: not-doctor + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?doctor"' + min: 0 + max: 0 + arm: both + - name: not-onboarding + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?onboarding"' + min: 0 + max: 0 + arm: both diff --git a/evals/trigger/zero-greenfield/case.yaml b/evals/trigger/zero-greenfield/case.yaml new file mode 100644 index 0000000..9cceda5 --- /dev/null +++ b/evals/trigger/zero-greenfield/case.yaml @@ -0,0 +1,31 @@ +schema_version: "1.1" +name: trigger-zero-greenfield +description: "greenfield 0→1 — 기능 요청(triage)·brownfield(onboarding)와 구분" +tags: [trigger, onboarding, triage, zero] +# skill 고르기만 재요 — scaffold·hook shim 없이 description 만으로 고르는지 봐요 (README '트리거 스위트') +plugins: ["../../.."] +execution: + prompt: "빈 리포에서 시작하는 새 제품이야. 코드 말고 아이디어부터 같이 정리하자" + max_turns: 4 + timeout_seconds: 180 + allowed_tools: [Skill] +graders: + - name: fires-zero + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?zero"' + min: 1 + - name: not-triage + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?triage"' + min: 0 + max: 0 + arm: both + - name: not-onboarding + type: tool_used + tool: Skill + input_match: '"skill"\s*:\s*"(?:[\w-]+:)?onboarding"' + min: 0 + max: 0 + arm: both diff --git a/skills/adr/SKILL.md b/skills/adr/SKILL.md index 3a424e4..2fda496 100644 --- a/skills/adr/SKILL.md +++ b/skills/adr/SKILL.md @@ -1,6 +1,6 @@ --- name: adr -description: "ADR (Architecture Decision Record) 작성 워크플로우. 새 결정을 .ax/docs/adr/-.md로 기록 (id = YYYY-MM-DD-<4hex>). 트리거: '/adr', 'ADR 작성', 'adr', '결정 기록', '아키텍처 결정', 'rationale', '선택의 근거'." +description: "되돌리기 비싼 설계·아키텍처 결정을 내렸거나 그 근거를 남겨야 할 때 써요. 트리거: '/adr', 'ADR 작성', 'adr', '결정 기록', '아키텍처 결정', 'rationale', '선택의 근거', '왜 이렇게 정했는지 남겨'. 안 쓰는 경우: 기능 단위 요구사항 문서(spec), 실수 기록(mistake)." --- # adr — ADR 작성 워크플로우 diff --git a/skills/audit/SKILL.md b/skills/audit/SKILL.md index 99e6d6b..86e8ecd 100644 --- a/skills/audit/SKILL.md +++ b/skills/audit/SKILL.md @@ -1,6 +1,6 @@ --- name: audit -description: "Mistake Loop **회고·승격** skill (read+aggregate, review phase). 누적된 .ax/mistakes/*.md 를 카테고리·빈도로 분석하고 룰 승격 후보 제시. 트리거: '/audit', 'goax audit', '실수 회고', '실수 분석', '실수 패턴 분석', 'mistakes 정리', 'mistakes 점검', '재발', '같은 문제', '룰 승격', '룰 강화 후보', '또 그걸', '주간 회고'. **'회고'·'심사'·'정리'·'패턴'·'승격'·'재발' 같은 review 의도 키워드만 매칭** — '기록'·'캡처'·'남겨' 같은 capture 의도는 mistake skill 이 담당. 1건 새로 캡처하지 않음 (mistake skill 위임)." +description: "쌓인 실수 기록(.ax/mistakes/)을 돌아보고 반복 패턴과 룰 승격 후보를 찾을 때 써요 — 여러 건을 보는 회고예요. 트리거: '/audit', 'goax audit', '실수 회고', '실수 분석', '실수 패턴 분석', 'mistakes 정리', 'mistakes 점검', '재발', '같은 문제', '또 그걸', '룰 승격', '룰 강화 후보', '주간 회고'. 안 쓰는 경우: 방금 생긴 실수 1건을 남기는 일('기록'·'캡처'·'남겨'·'박아'·'적어' — mistake), 룰이 실제로 걸려 있는지 점검(doctor)." --- # goax audit — Mistake Loop (캡처 → 심사 → 승격) diff --git a/skills/doctor/SKILL.md b/skills/doctor/SKILL.md index 2490fee..289a36c 100644 --- a/skills/doctor/SKILL.md +++ b/skills/doctor/SKILL.md @@ -1,6 +1,6 @@ --- name: doctor -description: "goax 설치 상태 진단 — 'goax doctor', 'goax 진단', '하네스 점검', 'goax 상태', 'spirit 점검', 'spirit lint', 'rules 보여줘', '룰 인덱스', 'CRITICAL 룰만' 트리거. 4계층 + cross-cut + sensors 결손, Spirit 무결성, 룰 통합 인덱스, 룰이 실제로 세션에 닿는지(도달 지도)를 스크립트로 검사하고 다음 단계를 안내. 슬래시로도 호출 가능: '/doctor'." +description: "goax 가 이 프로젝트에 제대로 깔려 돌아가는지 의심될 때, 또는 지금 걸린 룰과 spirit 을 훑어볼 때 써요. 트리거: 'goax doctor', 'goax 진단', '하네스 점검', 'goax 상태', '훅이 안 돌아', 'spirit 점검', 'spirit lint', 'rules 보여줘', '룰 인덱스', 'CRITICAL 룰만', '/doctor'. 안 쓰는 경우: 아직 설치 전이거나 새 버전으로 맞출 때(up), 실수 회고·룰 승격(audit), spec 문서 점검(spec-validate)." --- # goax doctor — 4계층 결손 진단 diff --git a/skills/hud/SKILL.md b/skills/hud/SKILL.md index 26c4dfa..65485ee 100644 --- a/skills/hud/SKILL.md +++ b/skills/hud/SKILL.md @@ -1,6 +1,6 @@ --- name: hud -description: "goax HUD(statusline) 관리 — 'HUD 활성화', 'statusline 설정', 'HUD 갱신', 'hud preset', 'HUD 미리보기', 'HUD 끄기', '/hud'. 하네스 위치만 한 줄로 보여줘요: 설치 버전 · triage Size×Risk · M 이상의 spec › tasks › impl › review 진행 단계 · 실수 누적. OMC HUD 와 같은 문법(프리셋 minimal/focused/full · ASCII 바)이고, 다른 statusline 과 합칠 땐 stdin 을 양쪽에 먹이는 합성 스크립트를 써요. 슬래시로도 호출 가능: '/hud'." +description: "goax 상태 표시줄(statusline)을 켜거나 끄거나 프리셋을 바꿀 때, 다른 statusline 과 합칠 때 써요. 트리거: '/hud', 'HUD 활성화', 'HUD 끄기', 'HUD 갱신', 'HUD 미리보기', 'hud preset', 'statusline 설정'. 안 쓰는 경우: 하네스 설치 상태 진단(doctor), spec 진행률 확인(spec-validate)." --- # hud — 하네스 위치 한 줄 diff --git a/skills/lane/SKILL.md b/skills/lane/SKILL.md index 9c4839f..848fe00 100644 --- a/skills/lane/SKILL.md +++ b/skills/lane/SKILL.md @@ -1,6 +1,6 @@ --- name: lane -description: "작업을 레인으로 가르고 게이트로 검증 — '/lane', 'lane', '레인', '병렬로 돌리자', 'parallel', '레인 나눠줘', '갈래로 쪼개', '동시에 진행', '나눠서 돌려줘', '에이전트 몇 개로', '핫 파일', '파일 소유권'. 먼저 **가를 수 있는 일인지** 판정하고 (아니면 단일 레인), 조사 국면은 역할/도메인으로, 실행 국면은 파일 소유권으로 가르고, tasks-plan.sh 의 violations 가 비어야 통과. 실행 자체는 spec-implement 가 해요. 슬래시로도 호출 가능: '/lane'." +description: "작업을 여러 에이전트로 나눠 돌릴 수 있을지, 어떻게 가를지 정할 때 써요 — 실행 전 판정이에요. 트리거: '/lane', 'lane', '레인', '레인 나눠줘', '병렬로 돌리자', 'parallel', '갈래로 쪼개', '동시에 진행', '나눠서 돌려줘', '에이전트 몇 개로', '핫 파일', '파일 소유권'. 안 쓰는 경우: 레인 배정이 끝난 tasks.md 를 실제로 돌릴 때('레인 실행'·'레인으로 돌려' — spec-implement), tasks.md 가 아직 없을 때(spec-tasks)." --- # lane — 레인 설계 + 게이트 diff --git a/skills/mistake/SKILL.md b/skills/mistake/SKILL.md index 9997fd1..78beb5c 100644 --- a/skills/mistake/SKILL.md +++ b/skills/mistake/SKILL.md @@ -1,6 +1,6 @@ --- name: mistake -description: "실수·결함을 사용자 의도대로 **1건 캡처** 하는 skill (write-only, capture phase). 트리거: '/mistake', '실수 기록해줘', '실수 남겨줘', 'mistake 캡처', 'mistake 박아줘', '이번 실수 적어줘', '방금 거 mistake 로 박아'. 사용자 인터뷰 (category/severity/detected_by/context_link/ONE_LINE) → init-mistake-file.sh 가 frontmatter 채움 → LLM 이 본문 (5 Whys / 영향 / audit 액션) Edit 으로 채움. **'기록'·'캡처'·'남겨'·'박기'·'적어' 같은 capture 의도 키워드만 매칭** — '회고'·'심사'·'룰 승격' 같은 review 의도는 audit skill 이 담당. 사용자가 capture 의도 표현 즉시 자동 발동." +description: "방금 일어난 실수·결함 1건을 기록으로 남기자고 할 때 바로 써요. 트리거: '/mistake', '실수 기록해줘', '실수 남겨줘', '이번 실수 적어줘', 'mistake 캡처', 'mistake 박아줘', '방금 거 mistake 로 박아'. 안 쓰는 경우: 쌓인 실수의 회고·패턴 분석·룰 승격('회고'·'분석'·'재발'·'승격' — audit), 그 버그를 지금 고쳐 달라는 요청('고쳐줘' — triage), 설계 결정 기록(adr)." --- # Mistake Capture Skill diff --git a/skills/onboarding/SKILL.md b/skills/onboarding/SKILL.md index ee233de..95584d2 100644 --- a/skills/onboarding/SKILL.md +++ b/skills/onboarding/SKILL.md @@ -1,6 +1,6 @@ --- name: onboarding -description: "/up (install or update) 직후 또는 사용자가 'goax 분석/마무리/세팅' 등을 말할 때 발동. .ax/.onboarding-pending 마커가 있으면 우선 처리. 프로젝트의 CLAUDE.md·모듈·도메인·외부 spec을 실제로 읽고 도메인 위험도(L0~L3) 매핑·hooks 강도·룰 분류를 사용자와 대화하며 .ax/config.yml과 adoption-plan.md에 기록. 트리거: '/onboarding', 'goax 도입 마무리', 'goax 분석', 'goax 마무리', '하네스 onboarding'." +description: "goax 를 깐 기존 프로젝트(brownfield)에서 도메인 위험도·룰·hook 강도를 프로젝트에 맞게 정리할 때 써요. up 직후 .ax/.onboarding-pending 이 남아 있으면 먼저 써요. 트리거: '/onboarding', 'goax 분석', 'goax 마무리', 'goax 도입 마무리', 'goax 세팅', '하네스 onboarding'. 안 쓰는 경우: 설치 자체(up), 빈 리포에서 새 제품 시작(zero), 설치 상태 진단(doctor)." --- # onboarding — 프로젝트 분석 + 5가지 결정 diff --git a/skills/spec-implement/SKILL.md b/skills/spec-implement/SKILL.md index 918d9f6..5850c1b 100644 --- a/skills/spec-implement/SKILL.md +++ b/skills/spec-implement/SKILL.md @@ -1,6 +1,6 @@ --- name: spec-implement -description: "tasks.md 의 - [ ] 항목을 실행하고 완료 시 - [x] 로 마킹 — '구현 시작', 'tasks 실행', 'task 진행', '레인 실행', '레인으로 돌려'. tasks.md 에 레인 배정(`레인:`)이 있으면 코디네이터로 lane-worker 를 띄우고 원장(lanes-dispatch.sh)에 디스패치·보고를 기록해요. 진입 시 tasks-plan.sh violations 검사, 완료 시 tasks-gate.sh G1~G6 + 새 컨텍스트 evaluator 게이트. 실패 시 halt+보고. friction 강도는 .ax/config.yml 의 confirmation 정책으로 결정. 슬래시로도 호출 가능: '/spec-implement'." +description: "tasks.md 의 미완료 항목(- [ ])을 실제로 실행할 때 써요 — 레인 배정이 끝난 tasks.md 를 돌릴 때도 이 skill 이에요. 트리거: '구현 시작', 'tasks 실행', 'task 진행', '레인 실행', '레인으로 돌려', '/spec-implement'. 안 쓰는 경우: tasks.md 가 아직 없을 때(spec-tasks), 레인을 어떻게 가를지 정할 때(lane), spec 없이 들어온 새 작업 요청 '구현해줘'(triage 가 먼저)." --- # spec-implement — 구현 단계 @@ -73,7 +73,11 @@ CONFLICT_N=$(echo "$LEDGER" | jq '.result.lane_file_conflicts | length') 멈춰 있어요 (이전 판까지의 실제 결함 — spec-implement 가 phase 를 안 썼어요). ```bash -bash .ax/scripts/bash/update-task.sh --phase implementing --json +# 이 작업의 id 를 진입할 때 한 번 잡아 두고 이후 갱신에 계속 넘겨요. 같은 프로젝트에서 다른 세션이 다른 작업을 +# 열면 current-task.json 의 "지금 작업" 이 바뀌는데, --task 를 주면 내 작업 파일(.ax/tasks/.json)만 고쳐요. +# triage 가 이 대화에서 연 작업 id 가 우선이에요 — 모를 때만 지금 작업을 읽어요 (그 사이 다른 세션이 바꿨을 수 있어요) +WORK_ID="${WORK_ID:-$(jq -r '.task_id // empty' .ax/current-task.json 2>/dev/null)}" # tasks.md 의 T0NN 과 다른 작업 id 예요 +bash .ax/scripts/bash/update-task.sh --phase implementing ${WORK_ID:+--task "$WORK_ID"} --json bash .ax/scripts/bash/update-state.sh >/dev/null 2>&1 || true # HUD 캐시 (review 단계 표시 여부) ``` @@ -333,7 +337,7 @@ phase 가 `implementing`/`review` 인데 `tasks-gate.sh` 가 아직 실패면, 띄우기 전에 phase 를 `review` 로 적어요 — HUD 체인의 `review ●` 가 여기서 켜져요: ```bash -bash .ax/scripts/bash/update-task.sh --phase review --json +bash .ax/scripts/bash/update-task.sh --phase review ${WORK_ID:+--task "$WORK_ID"} --json ``` evaluator 가 파일을 직접 써요. 코디네이터는 결과를 받아 적지 않아요 — 받아 적는 순간 검사받는 @@ -346,7 +350,7 @@ evaluator 가 파일을 직접 써요. 코디네이터는 결과를 받아 적 if [ "$COMPLETE" = "true" ]; then echo "✅ spec $SPEC 구현 완료." [ -f "$SPEC_DIR/review.md" ] && echo " ✅ evaluator verdict: $(head -1 "$SPEC_DIR/review.md")" - bash .ax/scripts/bash/reset-task.sh >/dev/null 2>&1 || true + bash .ax/scripts/bash/reset-task.sh ${WORK_ID:+--task "$WORK_ID"} >/dev/null 2>&1 || true echo " ✅ current-task.json reset → phase=idle" bash .ax/scripts/bash/status-note.sh --set now "" --json >/dev/null 2>&1 || true # 끝난 항목은 지워요 — SSOT 는 git log · ADR bash .ax/scripts/bash/status-note.sh --add next "spec $SPEC 완료 — 다음 작업은 triage 부터" --json >/dev/null 2>&1 || true diff --git a/skills/spec-tasks/SKILL.md b/skills/spec-tasks/SKILL.md index dfb4224..58fa1ed 100644 --- a/skills/spec-tasks/SKILL.md +++ b/skills/spec-tasks/SKILL.md @@ -1,6 +1,6 @@ --- name: spec-tasks -description: "기존 spec에 tasks.md 추가 + 분해 가이드 — 'tasks 분해', 'tasks.md 작성', '작업 분해', '체크리스트 만들어'. spec.md (§3 acceptance + §7.5 Technical Context) + 관련 ADR 기반. add-spec-files.sh 로 selective cp. 슬래시로도 호출 가능: '/spec-tasks'." +description: "spec.md 가 이미 있고 그걸 실행할 체크리스트(tasks.md)로 쪼개야 할 때 써요. 트리거: 'tasks 분해', 'tasks.md 작성', '작업 분해', '체크리스트 만들어', '/spec-tasks'. 안 쓰는 경우: spec 자체가 아직 없을 때(spec), 분해한 tasks 를 여러 에이전트로 가를지 정할 때(lane), tasks 를 실제로 실행할 때(spec-implement)." --- # spec-tasks — tasks.md 단계 @@ -137,7 +137,8 @@ task 가 있으면 그건 spec 에 없는 일을 하고 있다는 신호예요. ## 4. current-task.json 갱신 ```bash -bash .ax/scripts/bash/update-task.sh --phase tasks --json +WORK_ID="${WORK_ID:-}" # ← 이 대화에서 triage 가 연 작업 id 를 넣어요 (모르면 비워요 — 병렬 작업이 있으면 update-task 가 되물어요) +bash .ax/scripts/bash/update-task.sh --phase tasks ${WORK_ID:+--task "$WORK_ID"} --json ``` ## 5. 출력 diff --git a/skills/spec-validate/SKILL.md b/skills/spec-validate/SKILL.md index b68d076..1c0663e 100644 --- a/skills/spec-validate/SKILL.md +++ b/skills/spec-validate/SKILL.md @@ -1,6 +1,6 @@ --- name: spec-validate -description: "spec 명료성 게이팅 + 합의 리뷰 + 진행률 visibility — 'spec 확인', 'goax spec check', '스펙 게이트', '합의 리뷰', 'spec 리뷰', '--consensus'. spec.md 의 NEEDS CLARIFICATION + placeholder `<...>` + 빈 필수 섹션 3 항목을 게이팅하고, 그 뒤 size L/XL 은 architect·evaluator 를 새 컨텍스트로 병렬·독립 리뷰(검토 범위가 달라요 — architect 는 구조·대안, evaluator 는 AC·코드 현실; spec-review.sh 가 리뷰어별 verdict 파일과 sha 를 집계하고 재리뷰는 --delta 로 바뀐 곳만, 통과 뒤 오타는 --fixup), M 은 '--consensus' 로 선택. tasks.md 진행률 + AC 진행률을 visibility 로 노출. 슬래시로도 호출 가능: '/spec-validate'." +description: "spec 을 tasks 분해·구현으로 넘기기 전에 명료한지 확인하거나 합의 리뷰를 받을 때, 또는 spec 의 tasks·AC 진행률을 볼 때 써요. size L/XL spec 은 넘기기 전에 꼭 거쳐요. 트리거: 'spec 확인', 'spec 리뷰', '스펙 게이트', '합의 리뷰', 'goax spec check', '--consensus', 'spec 진행률', '/spec-validate'. 안 쓰는 경우: spec 을 새로 만들 때(spec), 구현이 끝난 코드의 완료 확인(spec-implement), 하네스 설치 상태 점검(doctor)." --- # goax spec-validate — 명료성 게이팅 + 진행률 visibility @@ -252,11 +252,12 @@ bash .ax/scripts/bash/update-state.sh --skill spec-validate # canonical(derive 명료성 통과 **그리고** 합의 리뷰 통과(필수일 때) 시 phase 진행, 미해소 시 blocked_by 기록 → 다음 skill (`spec-tasks` / `spec-implement`) 이 phase 보고 차단: ```bash +WORK_ID="${WORK_ID:-}" # ← 이 대화에서 triage 가 연 작업 id 를 넣어요 (모르면 비워요 — 병렬 작업이 있으면 update-task 가 되물어요) # 통과 — 명료성 + (required 면) spec-review pass -bash .ax/scripts/bash/update-task.sh --phase spec_checked --blocked-by '[]' --json +bash .ax/scripts/bash/update-task.sh --phase spec_checked ${WORK_ID:+--task "$WORK_ID"} --blocked-by '[]' --json bash .ax/scripts/bash/update-state.sh >/dev/null 2>&1 || true # HUD: spec ✓ › tasks ● # 미해소 — blocked_by 에 위치/카테고리 기록 (합의 리뷰 미통과도 여기) BLOCKED='["spec.md:42 NEEDS","spec.md:18 placeholder","review-spec: evaluator 보강 필요"]' -bash .ax/scripts/bash/update-task.sh --phase spec_blocked --blocked-by "$BLOCKED" --json +bash .ax/scripts/bash/update-task.sh --phase spec_blocked ${WORK_ID:+--task "$WORK_ID"} --blocked-by "$BLOCKED" --json ``` diff --git a/skills/spec/SKILL.md b/skills/spec/SKILL.md index 4b0fa75..78f3940 100644 --- a/skills/spec/SKILL.md +++ b/skills/spec/SKILL.md @@ -1,6 +1,6 @@ --- name: spec -description: "새 spec 디렉토리 생성 — 'spec 만들어줘', 'goax spec new ', '스펙 작성 시작'. `.ax/docs/spec/-/`(id = YYYY-MM-DD-<4hex>) 에 size×risk에 맞는 tier(standard/full)만큼만 SDD 산출물 생성. plan.md 폐기 — 설계 결정은 ADR 로. tier override: '--tier standard|full' 또는 자연어 '간단/풀패키지'. 결정론은 .ax/scripts/bash/ 위임. 슬래시로도 호출 가능: '/spec'." +description: "triage 가 M 이상으로 분류했거나 사용자가 spec 을 새로 쓰자고 할 때 써요 — 새 spec 디렉토리를 만들어요. 트리거: 'spec 만들어줘', '스펙 작성 시작', 'goax spec new ', '/spec', tier 지정('--tier standard|full', '간단', '풀패키지'). 안 쓰는 경우: 이미 있는 spec 을 체크리스트로 쪼개기(spec-tasks), spec 명료성 확인·리뷰(spec-validate), 설계 결정 하나 남기기(adr), 아직 분류 전인 새 작업 요청(triage)." --- # goax spec — 새 SDD 디렉토리 (tier-aware, script-backed) @@ -142,7 +142,8 @@ NEXT_STEP=$(echo "$RESULT" | jq -r '.next_step') ## 3.5. current-task.json 갱신 ```bash -bash .ax/scripts/bash/update-task.sh --phase spec \ +WORK_ID="${WORK_ID:-}" # ← 이 대화에서 triage 가 연 작업 id 를 넣어요 (모르면 비워요 — 병렬 작업이 있으면 update-task 가 되물어요) +bash .ax/scripts/bash/update-task.sh --phase spec ${WORK_ID:+--task "$WORK_ID"} \ --set "spec_id=$SPEC_ID" --set "spec_dir=$SPEC_DIR" --set "spec_tier=$TIER" --json ``` diff --git a/skills/triage/SKILL.md b/skills/triage/SKILL.md index 0d10d9e..306e94d 100644 --- a/skills/triage/SKILL.md +++ b/skills/triage/SKILL.md @@ -1,6 +1,6 @@ --- name: triage -description: "사용자가 새 작업·기능·수정·리팩토링·버그 fix를 요청할 때 가장 먼저 자동 매칭되는 skill. Size × Risk로 30초 안에 분류하고 spirit·룰·페르소나를 자동 주입. 트리거: '구현해줘', '만들어줘', '작업 계획', '어떻게 만들지', '고쳐줘', '추가해줘', '바꿔줘', '리팩토링', '리팩터링', 'fix', '버그', '기능 추가', 'PR 만들어', '작업하자', '/triage', 'classify', 새 대화 첫 메시지에서 작업 의도가 보이면 자동 발동. EnterPlanMode 안에서도 1회 호출 필수." +description: "새 작업 요청이 들어오면 코드를 건드리기 전에 가장 먼저 써요 — 기능 추가·수정·버그 fix·리팩토링 요청을 Size×Risk 로 분류해요. 트리거: '구현해줘', '만들어줘', '고쳐줘', '추가해줘', '바꿔줘', '리팩토링', '리팩터링', 'fix', '버그', '기능 추가', '작업 계획', '어떻게 만들지', 'PR 만들어', '작업하자', '/triage', 'classify'. 새 대화 첫 메시지에 작업 의도가 보이면 바로, EnterPlanMode 안에서도 한 번 써요. 안 쓰는 경우: 설명·질문만 하는 대화, 이미 분류된 작업의 spec 만들기(spec), tasks.md 실행('구현 시작' — spec-implement), 빈 리포에서 새 제품 정의(zero), 실수 기록(mistake)." --- # Triage Skill @@ -197,7 +197,7 @@ M×L2 에서 사용자가 합의 리뷰를 켜면 `intent_notes.consensus_review 분류 직후 `.ax/current-task.json`에 작업 컨텍스트를 기록해요. spec/spec-tasks/spec-implement/audit 이 이 파일을 *입력*으로 받음 (LLM 재추론 X). ```bash -TASK_ID=$(date -u +%Y-%m-%d)-$(printf '%03d' $((RANDOM % 1000))) +TASK_ID=$(date -u +%Y-%m-%d)-$(od -An -N2 -tx1 /dev/urandom | tr -d ' \n') # 4hex — 같은 날 병렬 작업끼리 안 겹치게 INTENT_JSON='{}' # 0단계 답변 — 축별 객체 (예: '{"why":"버그 수정 — 환불 실패"}'), 스킵 시 {} # M×L2 모호 영역에서 합의 리뷰를 켰으면 '{"consensus_review":"true"}' 도 병합 @@ -211,13 +211,20 @@ bash .ax/scripts/bash/update-task.sh --start --phase triaged \ # 사용자가 이 작업의 확인 강도를 말했으면 같이 적어요 (아래 "friction — 자연어로 받아요"). spec-implement 가 # config.yml 의 confirmation.mode 대신 이 값을 읽어요 (L3 는 여전히 override). # 말하지 않았으면 적지 않아요 — 기본값을 여기서 정하면 사용자가 config 를 바꿔도 반영되지 않아요. -[ -n "${FRICTION:-}" ] && bash .ax/scripts/bash/update-task.sh --set "friction=$FRICTION" --json +[ -n "${FRICTION:-}" ] && bash .ax/scripts/bash/update-task.sh --task "$TASK_ID" --set "friction=$FRICTION" --json # 계획은 어디서 세워도 돼요 (OMC plan · 다른 plan 도구 · 사람이 쓴 문서). 이 작업의 계획 문서가 있으면 경로를 적어요 — # spec 이 "원 계획" 으로 링크하고, spec 부터는 goax 가 관리해요. M 이상은 계획 문서가 있어도 spec 디렉토리를 만들어요. -[ -n "${PLAN_DOC:-}" ] && bash .ax/scripts/bash/update-task.sh --set "plan_doc=$PLAN_DOC" --json +[ -n "${PLAN_DOC:-}" ] && bash .ax/scripts/bash/update-task.sh --task "$TASK_ID" --set "plan_doc=$PLAN_DOC" --json ``` +**진행 중인 작업이 이미 있으면** (`phase` 가 `idle` 이 아니면) 새 작업을 `--start` 로 열어도 앞 작업은 사라지지 않아요 — +작업마다 `.ax/tasks/.json` 에 남고, current-task.json 은 지금 작업의 사본이에요. 앞 작업으로 돌아갈 땐 +`update-task.sh --task --activate`, 다른 세션의 작업을 건드리지 않고 내 작업만 고칠 땐 `--task ` 를 붙여요. +같은 작업을 이어 하는 거면 `--start` 하지 않아요 (같은 id 로 `--start` 하면 거부돼요). +**작업 id 를 사용자에게 한 번 보여주고 이 대화 내내 기억해요** — spec · spec-tasks · spec-validate · spec-implement 가 +`--task ` 로 넘겨요. 병렬 작업이 있는데 `--task` 가 없으면 update-task 가 덮지 않고 되물어요. + 이후 spec 이 `.ax/scripts/bash/tier-from-state.sh --json`로 tier 자동 결정. ### friction — 자연어로 받아요, 되묻지 않고 되비춰요 @@ -266,7 +273,7 @@ bash .ax/scripts/bash/update-task.sh --start --phase triaged \ ### 종료 후 — intent_notes 병합 ```bash -bash .ax/scripts/bash/update-task.sh --merge-intent "$REVERSE_INTERVIEW_JSON" --json # 같은 키면 새 값이 우선해요 +bash .ax/scripts/bash/update-task.sh --merge-intent "$REVERSE_INTERVIEW_JSON" --task "$TASK_ID" --json # 같은 키면 새 값이 우선해요 ``` spec/spec-tasks 는 이 `intent_notes` 를 입력으로 받아 §3 acceptance criteria 와 §7.5 Technical Context 를 채워요 — 재질문·재추론하지 않아요. diff --git a/skills/up/SKILL.md b/skills/up/SKILL.md index d5b95a8..48cfafe 100644 --- a/skills/up/SKILL.md +++ b/skills/up/SKILL.md @@ -1,6 +1,6 @@ --- name: up -description: "goax 프로젝트 install or idempotent update — '/up', 'goax up', 'goax 도입', 'goax 설치', 'goax 셋업', '하네스 적용' 등 자연어 트리거. 프로젝트를 분석하고 사용자 동의 후 .ax/ + AGENTS.md (Constitution SSOT) + CLAUDE.md (Claude Code alias) 를 설치. multi-CLI 지원 — Claude Code/OpenCode 환경 자동 감지, OpenCode 환경에선 opencode.json 추가 설치. brownfield 면 onboarding skill 로 이어감." +description: "goax 를 프로젝트에 처음 설치하거나, 깔린 goax 를 새 plugin 버전으로 맞출 때 써요. Claude Code·OpenCode 둘 다예요. 트리거: '/up', 'goax up', 'goax 설치', 'goax 도입', 'goax 셋업', 'goax 업데이트', '하네스 적용'. 안 쓰는 경우: 설치 뒤 도메인·룰 정리('goax 분석'·'goax 마무리' — onboarding), 설치 상태 진단(doctor), 빈 리포에서 새 제품 시작(zero), plugin 없이 저장소에 동봉('/vendor')." --- # goax up — install or idempotent update (4계층 하네스 도입) diff --git a/skills/vendor/SKILL.md b/skills/vendor/SKILL.md index 0847aa1..d4f5b55 100644 --- a/skills/vendor/SKILL.md +++ b/skills/vendor/SKILL.md @@ -1,6 +1,7 @@ --- name: vendor -description: "goax skill·command·agent 를 저장소에 동봉해서 plugin 설치 없이 팀 전체가 쓰게 만들어요. 모노레포처럼 ADE 루트(.claude/)와 프로젝트 루트(.ax/)가 다른 경우를 자동 처리. 트리거: '/vendor', 'goax 동봉', '플러그인 없이', '팀에 배포', 'vendoring', '스킬 복사', 'goax vendor'." +description: "goax skill·command·agent 를 저장소에 동봉해 plugin 설치 없이 팀 전체가 쓰게 할 때 써요 (모노레포 포함). 저장소에 파일을 한꺼번에 쓰는 일이라 사용자가 '/vendor' 로 직접 부를 때만 돌아요. 키워드: 'goax 동봉', 'goax vendor', 'vendoring', '플러그인 없이', '팀에 배포'." +disable-model-invocation: true --- # vendor — 저장소에 goax 동봉 diff --git a/skills/zero/SKILL.md b/skills/zero/SKILL.md index 1a18f78..b5102d1 100644 --- a/skills/zero/SKILL.md +++ b/skills/zero/SKILL.md @@ -1,6 +1,6 @@ --- name: zero -description: "zero to one — 아이디어 하나를 팔 수 있는 제품으로 끌고 가요. '/zero', 'zero to one', '0에서 시작', '새 제품 시작', '처음부터 만들자', '아이디어부터', 'PRD 부터', 'greenfield', '빈 리포에서 시작'. up 의 greenfield 분기가 넘겨받아요. 앞단(문제·대상·가치가설·안 만들 것·단위경제·성공중단기준·첫 사용자) → 중단(PRD·되돌리기 비싼 결정 ADR·시안 게이트) → 뒷단(domain_risk·스캐폴드·집행 배관·첫 배포·인계 노트). 대신 정하지 않고 역면접으로 끌어내요." +description: "빈 리포나 아이디어 단계에서 새 제품을 0부터 시작할 때 써요 — up 이 greenfield 로 판정하면 이어받아요. 트리거: '/zero', 'zero to one', '0에서 시작', '새 제품 시작', '처음부터 만들자', '아이디어부터', 'PRD 부터', 'greenfield', '빈 리포에서 시작'. 안 쓰는 경우: 이미 코드가 있는 프로젝트에 goax 를 맞출 때(onboarding), 기존 제품에 기능을 더하는 요청(triage)." --- # goax zero — 아이디어에서 팔 수 있는 것까지 diff --git a/templates/default/.ax/hooks/README.md b/templates/default/.ax/hooks/README.md index 1a1db7b..c1b97d5 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 캡처" 참고) · 활성 spec 완료 게이트 (스테이지 파일이 그 spec 디렉토리나 tasks.md `files:` 에 걸릴 때만 자세히 · 아니면 한 줄) | +| pre-commit/ | git commit 직전 | CRITICAL 룰 정적 검출 (위반 시 차단/경고만 — 자동 캡처는 폐기, §"Mistake 캡처" 참고) · 진행 중 작업들(current-task.json + `.ax/tasks/*.json`)의 spec 완료 게이트 (스테이지 파일이 그 spec 디렉토리나 tasks.md `files:` 에 걸릴 때만 자세히 · 아니면 한 줄) | | subagent-start/ | 서브에이전트가 뜨는 순간 | Constitution·Spirit·현재 spec·인계 노트 **경로**를 additionalContext 로 — 하네스가 메인 세션 밖으로 닿게 (goax 자기 에이전트는 제외) | | stop/ | 턴이 끝나려는 순간 | 활성 spec(implementing·review)이 완료 게이트 미통과면 **한 번** 멈춰 세우고 "마저 하기 · 보류 표기 · 인계 노트" 셋 중 하나를 시켜요 | @@ -72,6 +72,8 @@ compaction 뒤엔 4시간 TTL 이 다시 줄 여지를 남겨요. 세션 id 가 (`current-task.json` 의 `handoff.now`)에 그 spec 이 **24시간 안에** 적혀 있으면 멈추는 게 의도라고 보고 잡지 않아요. spec 은 ID(`2026-10-03-1b92`, 옛 순번 `014`)로 적어도 되고 디렉터리 전체 이름으로 적어도 돼요 — 다른 스크립트의 `--spec` 과 같은 기준이에요. ID 는 앞뒤가 영숫자가 아닐 때만 인정해요 (`014` 가 `T0140` 에 걸리지 않게). +Stop 게이트는 **지금 작업**(current-task.json)만 봐요 — 훅은 어느 작업이 이 세션 것인지 모르기 때문이에요. 병렬 작업의 +spec 은 커밋 때 `pre-commit/spec-completion-gate.sh` 가 보고, `tasks-gate.sh` G6 은 그 spec 을 맡은 작업(`.ax/tasks/*.json`)의 size×risk 로 판정해요. `status-note.sh --set now` 가 `handoff.now_at` 필드에 시각을 적고 게이트가 그 시각을 봐요. 시각이 없는 옛 노트는 인정하지 않아요 (예전엔 spec 이름만 있으면 통과라서 몇 주 전 노트 한 줄이 새 세션의 게이트를 영구히 꺼 버렸어요). 지울 땐 `status-note.sh --clear now`. 이 훅은 실행될 때 `.ax/.session/*` 의 24시간 넘은 diff --git a/templates/default/.ax/hooks/post-edit/lint-changed.sh b/templates/default/.ax/hooks/post-edit/lint-changed.sh index cf5871f..6ad1217 100755 --- a/templates/default/.ax/hooks/post-edit/lint-changed.sh +++ b/templates/default/.ax/hooks/post-edit/lint-changed.sh @@ -3,7 +3,8 @@ # # 명령: .ax/config.yml `commands.lint_file` — "<글롭> => <명령>" 목록, 첫 매칭 하나만. `{file}` 은 셸 인용된 상대 경로. # 비어 있으면 아무것도 안 해요 — 확장자로 도구를 추측하지 않아요. 후보는 detect-stack.sh. -# 결과: 통과면 조용히, 실패면 PostToolUse additionalContext 로 출력 앞 40줄·3000자 (막지 않아요). +# 결과: 통과면 조용히, 실패면 PostToolUse additionalContext 로 출력 앞 40줄·3000바이트 (막지 않아요). +# 자르기는 goax_cap_context — 한글 중간에서 안 끊고, 꼬리에 생략량과 직접 돌릴 명령을 적어요. # 시간: GOAX_LINT_TIMEOUT(30초) — timeout/gtimeout 이 있을 때만 끊어요. # 끄기: sensors.disabled_hooks 에 lint-changed, 또는 sensors.hook_profile: minimal set -uo pipefail # set -e 제거 — grep returning 1 (no match) 등이 hook 본체를 silent abort하지 않도록 @@ -62,7 +63,8 @@ fi NOTE="시간 초과(${TO}초)"; [ "$RC" -ne 124 ] && NOTE="exit $RC" CTX="[goax] lint 실패 — $REL ($NOTE) 명령: $RUN (commands.lint_file: \"$GLOB\") -$(printf '%s\n' "$OUT" | head -40 | cut -c1-300 | head -c 3000) +$(printf '%s\n' "$OUT" | head -40 | goax_cap_context 3000 "전체 출력은 위 명령을 직접 실행해서 보세요") 고친 파일의 문제면 지금 고치세요. 원래 있던 문제거나 명령이 잘못됐으면 사용자에게 알려요 — 설정을 느슨하게 하지 않아요." +CTX=$(printf '%s' "$CTX" | goax_cap_context) jq -nc --arg c "$CTX" '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$c}}' exit 0 diff --git a/templates/default/.ax/hooks/pre-bash/block-destructive.sh b/templates/default/.ax/hooks/pre-bash/block-destructive.sh index 0da8a0f..1d97594 100755 --- a/templates/default/.ax/hooks/pre-bash/block-destructive.sh +++ b/templates/default/.ax/hooks/pre-bash/block-destructive.sh @@ -52,6 +52,9 @@ if [ -f "$COMMON" ]; then source "$COMMON" SENSOR_MODE=$(goax_mode 2>/dev/null || echo warning) fi +type goax_cap_context >/dev/null 2>&1 || goax_cap_context() { cat; } # common.sh 없을 때 — 자르지 않아요 +# 막을 때 되돌려 주는 명령 원문 — heredoc 으로 큰 파일을 쓰는 명령이면 수십 KB 가 그대로 다시 들어가요. 앞부분만. +cmd_shown() { printf '%s' "$CMD" | goax_cap_context 600 "원문은 방금 보낸 명령 그대로예요"; } # ─── 매처 헬퍼 ────────────────────────────────────────────────────── # 정규화된 명령에 대해 확장 정규식 매칭. @@ -146,7 +149,7 @@ fi if [ -n "$CATASTROPHIC_HIT" ]; then printf '\033[31m[goax hook]\033[0m 🚨 CATASTROPHIC 명령 차단 (mode 무관): %s\n' "$CATASTROPHIC_HIT" >&2 - printf '명령: %s\n' "$CMD" >&2 + printf '명령: %s\n' "$(cmd_shown)" >&2 printf '복구 불가능한 삭제로 보여요. 의도한 게 맞으면 사용자에게 확인받고 직접 실행해.\n' >&2 exit 2 fi @@ -188,12 +191,12 @@ fi if [ -n "$RECOVERABLE_HIT" ]; then if [ "$SENSOR_MODE" = "fail" ]; then printf '\033[31m[goax hook]\033[0m 차단된 파괴적 패턴 (mode=fail): %s\n' "$RECOVERABLE_HIT" >&2 - printf '명령: %s\n' "$CMD" >&2 + printf '명령: %s\n' "$(cmd_shown)" >&2 printf '우회가 필요하면 사용자에게 명시적 승인을 받아 직접 실행해.\n' >&2 exit 2 fi printf '\033[33m[goax hook]\033[0m ⚠ 파괴적 패턴 (mode=%s, 경고만): %s\n' "$SENSOR_MODE" "$RECOVERABLE_HIT" >&2 - printf '명령: %s\n' "$CMD" >&2 + printf '명령: %s\n' "$(cmd_shown)" >&2 exit 0 fi diff --git a/templates/default/.ax/hooks/pre-bash/block-hook-bypass.sh b/templates/default/.ax/hooks/pre-bash/block-hook-bypass.sh index 6812553..87debc2 100644 --- a/templates/default/.ax/hooks/pre-bash/block-hook-bypass.sh +++ b/templates/default/.ax/hooks/pre-bash/block-hook-bypass.sh @@ -48,7 +48,9 @@ fi [ -z "$REASON" ] && exit 0 printf '\033[31m[goax hook]\033[0m 🚫 git 훅 우회 차단: %s\n' "$REASON" >&2 -printf '명령: %s\n' "$CMD" >&2 +# 막을 때 되돌려 주는 명령 원문 — heredoc 으로 큰 파일을 쓰는 명령이면 수십 KB 가 그대로 다시 들어가요. 앞부분만. +cmd_shown() { printf '%s' "$CMD" | goax_cap_context 600 "원문은 방금 보낸 명령 그대로예요"; } +printf '명령: %s\n' "$(cmd_shown)" >&2 printf '프로젝트의 git 훅은 룰 집행 장치예요 (enforced_by: hook:* · external:*). 에이전트가 끄면 집행이 사라져요.\n' >&2 printf '훅이 실패했다면 그 원인을 고치세요. 정말 우회해야 하면 사용자에게 직접 실행해 달라고 요청하세요 (프롬프트에서 `! <명령>`).\n' >&2 exit 2 diff --git a/templates/default/.ax/hooks/pre-bash/destructive-facts.sh b/templates/default/.ax/hooks/pre-bash/destructive-facts.sh index dfda704..229dcfb 100644 --- a/templates/default/.ax/hooks/pre-bash/destructive-facts.sh +++ b/templates/default/.ax/hooks/pre-bash/destructive-facts.sh @@ -69,10 +69,12 @@ if [ "$N" -gt 3 ]; then exit 2 fi +# 막을 때 되돌려 주는 명령 원문 — heredoc 으로 큰 파일을 쓰는 명령이면 수십 KB 가 그대로 다시 들어가요. 앞부분만. +cmd_shown() { printf '%s' "$CMD" | goax_cap_context 600 "원문은 방금 보낸 명령 그대로예요"; } { printf '\033[33m[goax hook]\033[0m ✋ 되돌리기 어려운 명령이에요 — 실행 전에 사실을 먼저 적어 주세요 (이번 세션 %s번째).\n' "$N" printf '%s\n' "$HITS" | sed 's/^/ 감지: /' - printf ' 명령: %s\n' "$CMD" + printf ' 명령: %s\n' "$(cmd_shown)" printf ' 1. 이 명령이 지우거나 되돌릴 파일 목록 — 추적 안 되는(untracked) 파일, 내가 만들지 않은 파일은 특히 (`git status --porcelain <경로>` 로 확인)\n' printf ' 2. 되돌리는 절차 한 줄 (없으면 "복구 불가" 라고 적기)\n' printf ' 3. 이 작업을 지시한 사용자 메시지 원문 인용\n' 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 9c91f4d..3c6b492 100755 --- a/templates/default/.ax/hooks/pre-commit/spec-completion-gate.sh +++ b/templates/default/.ax/hooks/pre-commit/spec-completion-gate.sh @@ -30,90 +30,106 @@ MODE=$(goax_mode 2>/dev/null || echo warning) command -v jq >/dev/null 2>&1 || exit 0 -# 활성 spec 이 있을 때만 — 없으면 검사할 대상이 없어요 +# 진행 중인 작업들의 spec — 지금 작업(current-task.json)과 병렬 작업(.ax/tasks/*.json) 중 phase ≠ idle 이고 +# spec_dir 이 있는 것. 같은 spec 은 한 번만 봐요. 병렬 작업은 GOAX_TASK_TTL_DAYS(기본 7일) 안에 갱신된 것만 — +# 버려진 작업이 커밋마다 검사되면 안 돼요 (정리는 reset-task.sh --task , session-brief 가 알려줘요). TASK_FILE="$PROJECT_ROOT/.ax/current-task.json" -[ -f "$TASK_FILE" ] || exit 0 -PHASE=$(jq -r '.phase // "idle"' "$TASK_FILE" 2>/dev/null || echo idle) -[ "$PHASE" = "idle" ] && exit 0 +TASK_TTL=$(( ${GOAX_TASK_TTL_DAYS:-7} * 86400 )) +SPECS=$({ jq -r 'select((.phase // "idle") != "idle") | .spec_dir // empty' "$TASK_FILE" 2>/dev/null + for f in "$PROJECT_ROOT"/.ax/tasks/*.json; do + [ -f "$f" ] || continue + jq -r --argjson ttl "$TASK_TTL" 'select((.phase // "idle") != "idle" + and (now - (((.updated_at // "") | try fromdateiso8601 catch 0))) < $ttl) | .spec_dir // empty' "$f" 2>/dev/null + done; } | while IFS= read -r d; do [ -n "$d" ] && basename "$d"; done | awk '!seen[$0]++') +[ -n "$SPECS" ] || exit 0 -OUT=$(bash "$GATE" --json 2>/dev/null || true) -[ -z "$OUT" ] && exit 0 -printf '%s' "$OUT" | jq -e '.result' >/dev/null 2>&1 || exit 0 +check_spec() { # $1 = spec 이름 — 막아야 하면 2 + OUT=$(bash "$GATE" --spec "$1" --json 2>/dev/null || true) + [ -z "$OUT" ] && return 0 + printf '%s' "$OUT" | jq -e '.result' >/dev/null 2>&1 || return 0 -# skipped/error 는 result 가 항상 {} — 검사 못 했다는 뜻이지 위반이 아니에요. -# tasks-gate.sh 가 이미 자기 next_step 으로 사유를 말하니 여기선 조용히 통과. -STATUS=$(printf '%s' "$OUT" | jq -r '.status // ""') -case "$STATUS" in - ok|warning) ;; - *) exit 0 ;; -esac + # skipped/error 는 result 가 항상 {} — 검사 못 했다는 뜻이지 위반이 아니에요. + # tasks-gate.sh 가 이미 자기 next_step 으로 사유를 말하니 여기선 조용히 통과. + STATUS=$(printf '%s' "$OUT" | jq -r '.status // ""') + case "$STATUS" in + ok|warning) ;; + *) return 0 ;; + esac -SPEC=$(printf '%s' "$OUT" | jq -r '.result.spec // ""') -OPEN=$(printf '%s' "$OUT" | jq -r '.result.open // 0') -PAUSED=$(printf '%s' "$OUT" | jq -r '.result.paused // 0') -DONE=$(printf '%s' "$OUT" | jq -r '.result.done // 0') -TOTAL=$(printf '%s' "$OUT" | jq -r '.result.total // 0') -UNCOV=$(printf '%s' "$OUT" | jq -r '(.result.ac_uncovered // []) | join(", ")') -DROP=$(printf '%s' "$OUT" | jq -r '.result.task_count_drop // 0') -UNREP=$(printf '%s' "$OUT" | jq -r '(.result.dispatched_unreported // []) | join(", ")') -NOREP=$(printf '%s' "$OUT" | jq -r '(.result.done_without_report // []) | join(", ")') -REQ=$(printf '%s' "$OUT" | jq -r '.result.review_required // false') -VERDICT=$(printf '%s' "$OUT" | jq -r '.result.review_verdict // ""') -VIOL=$(printf '%s' "$OUT" | jq -r '.result.violations // 0') + SPEC=$(printf '%s' "$OUT" | jq -r '.result.spec // ""') + OPEN=$(printf '%s' "$OUT" | jq -r '.result.open // 0') + PAUSED=$(printf '%s' "$OUT" | jq -r '.result.paused // 0') + DONE=$(printf '%s' "$OUT" | jq -r '.result.done // 0') + TOTAL=$(printf '%s' "$OUT" | jq -r '.result.total // 0') + UNCOV=$(printf '%s' "$OUT" | jq -r '(.result.ac_uncovered // []) | join(", ")') + DROP=$(printf '%s' "$OUT" | jq -r '.result.task_count_drop // 0') + UNREP=$(printf '%s' "$OUT" | jq -r '(.result.dispatched_unreported // []) | join(", ")') + NOREP=$(printf '%s' "$OUT" | jq -r '(.result.done_without_report // []) | join(", ")') + REQ=$(printf '%s' "$OUT" | jq -r '.result.review_required // false') + VERDICT=$(printf '%s' "$OUT" | jq -r '.result.review_verdict // ""') + VIOL=$(printf '%s' "$OUT" | jq -r '.result.violations // 0') -[ "${VIOL:-0}" -eq 0 ] && exit 0 + [ "${VIOL:-0}" -eq 0 ] && return 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") + # 이번 커밋이 그 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 + return 0 + fi 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 + + 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 + [ "${OPEN:-0}" -gt 0 ] && printf ' · 미완료 task %s개 — 끝내거나 `- [~] … 보류: <사유>` 로 표기하세요\n' "$OPEN" >&2 + [ -n "$UNCOV" ] && printf ' · 대응 task 가 없는 수용 기준: %s\n' "$UNCOV" >&2 + if [ "${DROP:-0}" -gt 0 ]; then + printf '\033[31m · task %s개가 사라졌어요\033[0m — 미완료를 지워서 통과시키는 건 안 돼요.\n' "$DROP" >&2 + printf ' 의도적으로 범위를 줄인 거면 spec.md 의 수용 기준도 같이 줄이세요.\n' >&2 + fi + [ -n "$UNREP" ] && printf ' · 보고 안 받은 디스패치: %s — 산출물을 받고 lanes-dispatch.sh --report 로 기록하세요\n' "$UNREP" >&2 + [ -n "$NOREP" ] && printf '\033[31m · 보고 없이 완료 표시: %s\033[0m — 레인이 자기 체크박스를 켰어요. 코디네이터가 검증 뒤에 켜야 해요\n' "$NOREP" >&2 + if [ "$REQ" = "true" ] && [ -z "$VERDICT" ]; then + printf ' · evaluator 리뷰 필수 (size×risk) — review.md 가 없어요. 새 컨텍스트로 evaluator 를 띄우세요\n' >&2 + elif [ -n "$VERDICT" ] && [ "$VERDICT" != "진행" ]; then + printf ' · evaluator verdict "%s" — 지적을 task 로 옮기거나 재논의한 뒤 커밋하세요\n' "$VERDICT" >&2 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 -[ "${OPEN:-0}" -gt 0 ] && printf ' · 미완료 task %s개 — 끝내거나 `- [~] … 보류: <사유>` 로 표기하세요\n' "$OPEN" >&2 -[ -n "$UNCOV" ] && printf ' · 대응 task 가 없는 수용 기준: %s\n' "$UNCOV" >&2 -if [ "${DROP:-0}" -gt 0 ]; then - printf '\033[31m · task %s개가 사라졌어요\033[0m — 미완료를 지워서 통과시키는 건 안 돼요.\n' "$DROP" >&2 - printf ' 의도적으로 범위를 줄인 거면 spec.md 의 수용 기준도 같이 줄이세요.\n' >&2 -fi -[ -n "$UNREP" ] && printf ' · 보고 안 받은 디스패치: %s — 산출물을 받고 lanes-dispatch.sh --report 로 기록하세요\n' "$UNREP" >&2 -[ -n "$NOREP" ] && printf '\033[31m · 보고 없이 완료 표시: %s\033[0m — 레인이 자기 체크박스를 켰어요. 코디네이터가 검증 뒤에 켜야 해요\n' "$NOREP" >&2 -if [ "$REQ" = "true" ] && [ -z "$VERDICT" ]; then - printf ' · evaluator 리뷰 필수 (size×risk) — review.md 가 없어요. 새 컨텍스트로 evaluator 를 띄우세요\n' >&2 -elif [ -n "$VERDICT" ] && [ "$VERDICT" != "진행" ]; then - printf ' · evaluator verdict "%s" — 지적을 task 로 옮기거나 재논의한 뒤 커밋하세요\n' "$VERDICT" >&2 -fi + if [ "$MODE" = "fail" ]; then + printf ' 차단됨 (sensors.mode=fail). 우회가 필요하면 사용자가 직접 커밋해요 (`! git commit --no-verify …`) — 에이전트의 --no-verify 는 block-hook-bypass 가 막아요.\n' >&2 + return 2 + fi + return 0 +} -if [ "$MODE" = "fail" ]; then - printf ' 차단됨 (sensors.mode=fail). 우회가 필요하면 사용자가 직접 커밋해요 (`! git commit --no-verify …`) — 에이전트의 --no-verify 는 block-hook-bypass 가 막아요.\n' >&2 - exit 2 -fi -exit 0 +RC=0 +while IFS= read -r sp; do + [ -n "$sp" ] || continue + check_spec "$sp" || RC=$? +done <<< "$SPECS" +exit "$RC" diff --git a/templates/default/.ax/hooks/pre-edit/module-rules-inject.sh b/templates/default/.ax/hooks/pre-edit/module-rules-inject.sh index 71536b5..74bce88 100755 --- a/templates/default/.ax/hooks/pre-edit/module-rules-inject.sh +++ b/templates/default/.ax/hooks/pre-edit/module-rules-inject.sh @@ -88,6 +88,8 @@ fi CTX="[goax] 이 파일에 걸린 상위 계층이에요. 편집 전에 해당 파일을 Read 하세요. ${LINES}" +# 모듈·ADR 이 많아도 10,000자 상한(넘으면 파일로 빠져요) 아래로 +CTX=$(printf '%s' "$CTX" | goax_cap_context "" "전체 목록: bash .ax/scripts/bash/rules-index.sh --source module") if command -v jq >/dev/null 2>&1; then jq -nc --arg c "$CTX" \ diff --git a/templates/default/.ax/hooks/pre-edit/spirit-rules-inject.sh b/templates/default/.ax/hooks/pre-edit/spirit-rules-inject.sh index bb6e9e0..8a168b6 100755 --- a/templates/default/.ax/hooks/pre-edit/spirit-rules-inject.sh +++ b/templates/default/.ax/hooks/pre-edit/spirit-rules-inject.sh @@ -38,6 +38,7 @@ fi type goax_hook_enabled >/dev/null 2>&1 && { goax_hook_enabled spirit-rules-inject standard || exit 0; } type goax_normalize_path >/dev/null 2>&1 || goax_normalize_path() { printf '%s' "${1:-}"; } type goax_inject_fresh >/dev/null 2>&1 || goax_inject_fresh() { return 0; } +type goax_cap_context >/dev/null 2>&1 || goax_cap_context() { head -c "${GOAX_CONTEXT_MAX:-8000}"; } # common.sh 없을 때 — 바이트 상한만 TARGET_ABS=$(goax_normalize_path "$TARGET_PATH" "$PROJECT_ROOT") ROOT_ABS=$(goax_normalize_path "$PROJECT_ROOT" "$PROJECT_ROOT") @@ -151,6 +152,8 @@ for m in "${FRESH[@]}"; do LIST+="- $m"$'\n' done CTX="📋 Path-scoped spirit rules apply to ${TARGET_REL} — Read these before editing if not yet:"$'\n'"$LIST" +# 룰 파일이 많아도 10,000자 상한(넘으면 파일로 빠져요) 아래로 +CTX=$(printf '%s' "$CTX" | goax_cap_context "" "전체 목록: bash .ax/scripts/bash/rules-index.sh --source spirit") # JSON 출력 if command -v jq >/dev/null 2>&1; then diff --git a/templates/default/.ax/hooks/session-start/session-brief.sh b/templates/default/.ax/hooks/session-start/session-brief.sh index e2c92b3..1eedbe9 100644 --- a/templates/default/.ax/hooks/session-start/session-brief.sh +++ b/templates/default/.ax/hooks/session-start/session-brief.sh @@ -39,5 +39,7 @@ LINES=$(GOAX_PROJECT_DIR="$PROJECT_ROOT" bash "$BRIEF" --json ${MODE_ARGS[@]+"${ CTX="[goax] 세션 브리핑 $(printf '%s\n' "$LINES" | sed 's/^/- /')" +# 스크립트 상한(GOAX_SESSION_BRIEF_MAX)을 크게 잡아도 10,000자 상한(넘으면 파일로 빠져요) 아래로 +CTX=$(printf '%s' "$CTX" | goax_cap_context "" "전체: bash .ax/scripts/bash/session-brief.sh") jq -nc --arg c "$CTX" '{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:$c}}' exit 0 diff --git a/templates/default/.ax/hooks/subagent-start/harness-pointer.sh b/templates/default/.ax/hooks/subagent-start/harness-pointer.sh index 358d09b..134f8e3 100755 --- a/templates/default/.ax/hooks/subagent-start/harness-pointer.sh +++ b/templates/default/.ax/hooks/subagent-start/harness-pointer.sh @@ -66,6 +66,8 @@ fi CTX="[goax] 이 프로젝트에는 하네스가 있어요. 편집이나 판단 전에 아래를 Read 하세요 (경로만 드려요): ${LINES}규율: 커밋하지 않아요 · 브리프에 소유 파일 목록이 있으면 그 밖은 건드리지 않아요 · 정량 주장엔 센 명령과 출력을 붙여요." +# 경로 몇 줄이라 보통 짧지만, 상한은 다른 주입 훅과 같은 함수로 지켜요 +type goax_cap_context >/dev/null 2>&1 && CTX=$(printf '%s' "$CTX" | goax_cap_context) jq -nc --arg c "$CTX" '{hookSpecificOutput:{hookEventName:"SubagentStart", additionalContext:$c}}' exit 0 diff --git a/templates/default/.ax/scripts/bash/README.md b/templates/default/.ax/scripts/bash/README.md index f5ea470..41bbfaf 100644 --- a/templates/default/.ax/scripts/bash/README.md +++ b/templates/default/.ax/scripts/bash/README.md @@ -11,7 +11,7 @@ | `detect-model.sh` | 지금 돌고 있는 모델 식별 — override → `$GOAX_MODEL` → transcript 스캔 → unknown | (진단·로깅용) | | `next-spec-num.sh` | 새 spec/ADR ID 발급 — `YYYY-MM-DD-<4hex>` (`--kind spec\|adr`, `--reserve --slug` 로 실물까지 O_EXCL 생성). 순번이 아니라 브랜치끼리 안 겹쳐요. `--check-duplicates` 는 옛 순번(`NNN`/`NNNN`) 중복 진단 | `spec`, `adr`, `doctor` | | `tier-from-state.sh` | current-task.json + config.yml → tier 결정 + evaluator 필수 여부 + spec_review 필수 여부(Size 축만) (`--reset` 는 `reset-task.sh` 경유) | `spec`, `tasks-gate.sh`, `spec-review.sh`, `update-state.sh` | -| `update-task.sh` | `current-task.json` 의 task 필드를 **락 안에서 in-place** 갱신 — `--phase

` · `--set task_id\|description\|size\|risk\|domain\|spec_id\|spec_dir\|spec_tier\|friction\|plan_doc=` (`plan_doc` = 외부 계획 문서 경로 — spec 의 "원 계획") · `--blocked-by ''` · `--merge-intent ''` · `--start`. enum(size·risk·spec_tier·phase) 검증 실패면 아무것도 안 씀. SKILL.md 의 인라인 jq 를 대체 — 인라인은 무락이라 `handoff` 를 잃어요 | `triage`, `spec`, `spec-validate`, `spec-tasks`, `spec-implement` | +| `update-task.sh` | `current-task.json` 의 task 필드를 **락 안에서 in-place** 갱신 — 작업별 원본은 `.ax/tasks/.json` (`--task ` 면 그 작업만 · 지금 작업이 아니면 current-task.json 은 그대로 · `--activate` 로 지금 작업 전환) — `--phase

` · `--set task_id\|description\|size\|risk\|domain\|spec_id\|spec_dir\|spec_tier\|friction\|plan_doc=` (`plan_doc` = 외부 계획 문서 경로 — spec 의 "원 계획") · `--blocked-by ''` · `--merge-intent ''` · `--start`. enum(size·risk·spec_tier·phase) 검증 실패면 아무것도 안 씀. SKILL.md 의 인라인 jq 를 대체 — 인라인은 무락이라 `handoff` 를 잃어요 | `triage`, `spec`, `spec-validate`, `spec-tasks`, `spec-implement` | | `init-spec-dir.sh` | tier별 selective spec 디렉토리 생성 | `spec` | | `add-spec-files.sh` | 기존 spec에 tasks/research 등 점진 추가 | `spec-tasks`, `spec --add` | | `slug-from-text.sh` | 영문 텍스트 → kebab-case 정규화·검증 | `spec` | @@ -24,7 +24,7 @@ | `init-mistake-file.sh` | mistake 파일 skeleton 생성 (template cp + frontmatter 치환) | `mistake`, `audit` | | `install-git-hooks.sh` | `.ax/hooks/pre-commit/*.sh` chain 을 git pre-commit wrapper 로 설치 (모든 환경 기본 — 사람 터미널 커밋 커버) | `up`, `onboarding` | | `register-spirit-hook.sh` | `.claude/settings.json` 에 spirit-rules-inject hook idempotent 등록 | `doctor` | -| `reset-task.sh` | 작업 완료 후 `current-task.json` → phase=idle 리셋 (`tier-from-state.sh --reset` 위임). `handoff` 는 남기고, 파일이 없으면 만들지 않고 exit 1 | `spec-implement` | +| `reset-task.sh` | 작업 완료 후 `current-task.json` → phase=idle 리셋 (`tier-from-state.sh --reset` 위임). 그 작업의 `.ax/tasks/.json` 은 지워요 · `--task ` 가 지금 작업이 아니면 그 파일만. `handoff` 는 남기고, 파일이 없으면 만들지 않고 exit 1 | `spec-implement` | | `session-brief.sh` | 세션 첫머리 브리핑 — 진행 중 task · 인계 노트(now·next·open 앞 3개) · 설치본 < 플러그인 버전 · 밀린 audit · 같은 category 실수 재발. 말할 게 없으면 빈 출력, 글자 상한 `--max-chars`(기본 1200). `--snapshot --session ` 는 압축 직전 사실(브랜치·HEAD·바뀐 파일·tasks 진행률)을 적고, `--after-compact` 가 한 번 앞에 붙여요 | SessionStart 훅 `session-start/session-brief.sh`, PreCompact 훅 `pre-compact/snapshot.sh` | | `update-state.sh` | `.ax/` 실측 → `.ax/state.json` (layers/cross_cut/sensors_mode + HUD 캐시 `hud.{plugin_version,review_required,cached_at}`) 갱신. `--skill ` 이 `last_skill`·`skill_calls+=1` 을, `--last-mistake ` 이 `last_mistake_file` 을 **같은 락·같은 쓰기** 안에서 찍어요 — SKILL.md 가 state.json 을 인라인 jq 로 쓰면 안 돼요 (smoke 가 막아요) | 모든 skill 의 마무리 (`--skill <자기 이름>`) | | `triage-search.sh` | KEYWORDS 로 6 군데(specs/adrs/mistakes/rules/modules/imported) 검색 + 동의어 확장 + 매칭수 랭킹 + 스니펫 + 도메인 boost | `triage` | diff --git a/templates/default/.ax/scripts/bash/add-spec-files.sh b/templates/default/.ax/scripts/bash/add-spec-files.sh index b5f21d8..267b594 100755 --- a/templates/default/.ax/scripts/bash/add-spec-files.sh +++ b/templates/default/.ax/scripts/bash/add-spec-files.sh @@ -62,7 +62,7 @@ if [ "$RC" -eq 2 ]; then goax_error "--spec '$SPEC' 이 여러 spec 에 걸려요: ${GOAX_SPEC_CANDIDATES} — 하나를 정확히 적으세요" exit "$EXIT_ERROR" elif [ "$RC" -ne 0 ]; then - goax_error "spec not found: $SPEC" + goax_error "spec 을 못 찾았어요: '$SPEC' — 목록은 ls .ax/docs/spec/ · 새 spec 은 spec skill 로 먼저 만들어요" exit "$EXIT_ERROR" fi SPEC_DIR="$PROJECT_ROOT/.ax/docs/spec/$RESOLVED" diff --git a/templates/default/.ax/scripts/bash/ade-settings.sh b/templates/default/.ax/scripts/bash/ade-settings.sh index b929856..88d7df6 100644 --- a/templates/default/.ax/scripts/bash/ade-settings.sh +++ b/templates/default/.ax/scripts/bash/ade-settings.sh @@ -132,7 +132,7 @@ if [ "$MODE" = apply ] && { [ -n "$MISSING" ] || [ -n "$STALE" ]; }; then | map(select((.hooks | length) > 0)))) | with_entries(select((.value | length) > 0))) as $kept | .hooks = (reduce ($exp | to_entries[]) as $e ($kept; .[$e.key] = ((.[$e.key] // []) + $e.value)))' > "$TMP"; then - rm -f "$TMP"; goax_unlock "$LOCK"; goax_error "settings 병합 실패 — 아무것도 안 바꿨어요"; exit "$EXIT_ERROR" + rm -f "$TMP"; goax_unlock "$LOCK"; goax_error "settings 병합 실패 — 아무것도 안 바꿨어요. 현재 상태는 bash .ax/scripts/bash/ade-settings.sh --check --json"; exit "$EXIT_ERROR" fi mv "$TMP" "$SETTINGS" goax_unlock "$LOCK" diff --git a/templates/default/.ax/scripts/bash/check-rule-enforcement.sh b/templates/default/.ax/scripts/bash/check-rule-enforcement.sh index c13114c..26db289 100755 --- a/templates/default/.ax/scripts/bash/check-rule-enforcement.sh +++ b/templates/default/.ax/scripts/bash/check-rule-enforcement.sh @@ -80,8 +80,8 @@ if [ "$SHOW_HELP" = true ]; then fi if ! command -v jq >/dev/null 2>&1; then - if [ "$JSON_MODE" = true ]; then json_error "jq 미설치" - else goax_error "jq 미설치 — invariant 검증 불가"; exit "$EXIT_ERROR"; fi + if [ "$JSON_MODE" = true ]; then json_error "jq 가 필요해요 — brew install jq (macOS) · apt-get install jq (Debian/Ubuntu) 뒤 다시" + else goax_error "jq 가 필요해요 — brew install jq (macOS) · apt-get install jq (Debian/Ubuntu) 뒤 다시 (invariant 검증 불가)"; exit "$EXIT_ERROR"; fi fi ROOT=$(find_project_root) || exit "$EXIT_ERROR" diff --git a/templates/default/.ax/scripts/bash/check-sensor-liveness.sh b/templates/default/.ax/scripts/bash/check-sensor-liveness.sh index 8046862..602a837 100755 --- a/templates/default/.ax/scripts/bash/check-sensor-liveness.sh +++ b/templates/default/.ax/scripts/bash/check-sensor-liveness.sh @@ -56,8 +56,8 @@ if [ "$SHOW_HELP" = true ]; then fi if ! command -v jq >/dev/null 2>&1; then - if [ "$JSON_MODE" = true ]; then json_error "jq 미설치" - else goax_error "jq 미설치 — liveness 검증 불가"; exit "$EXIT_ERROR"; fi + if [ "$JSON_MODE" = true ]; then json_error "jq 가 필요해요 — brew install jq (macOS) · apt-get install jq (Debian/Ubuntu) 뒤 다시" + else goax_error "jq 가 필요해요 — brew install jq (macOS) · apt-get install jq (Debian/Ubuntu) 뒤 다시 (liveness 검증 불가)"; exit "$EXIT_ERROR"; fi fi ROOT=$(find_project_root) || exit "$EXIT_ERROR" diff --git a/templates/default/.ax/scripts/bash/check-spec-clarity.sh b/templates/default/.ax/scripts/bash/check-spec-clarity.sh index 1faed3d..9601579 100755 --- a/templates/default/.ax/scripts/bash/check-spec-clarity.sh +++ b/templates/default/.ax/scripts/bash/check-spec-clarity.sh @@ -85,9 +85,9 @@ fi if [ ! -f "$TARGET" ]; then if [ "$JSON_MODE" = true ]; then - json_error "file not found: $TARGET" + json_error "파일이 없어요: $TARGET — spec 목록은 ls .ax/docs/spec/ · --spec 로 주면 그 spec.md 를 찾아요" else - goax_error "file not found: $TARGET" + goax_error "파일이 없어요: $TARGET — spec 목록은 ls .ax/docs/spec/ · --spec 로 주면 그 spec.md 를 찾아요" exit "$EXIT_ERROR" fi fi diff --git a/templates/default/.ax/scripts/bash/common.sh b/templates/default/.ax/scripts/bash/common.sh index 7f210ec..c840f11 100755 --- a/templates/default/.ax/scripts/bash/common.sh +++ b/templates/default/.ax/scripts/bash/common.sh @@ -41,6 +41,61 @@ function clip(s, n, nw, w, out, cand, i) { } ' +# ─── 훅이 모델에게 주는 글의 길이 상한 — goax_cap_context ─────────────── +# printf '%s' "$CTX" | goax_cap_context [최대 바이트] [전체를 볼 곳] +# Claude Code 는 훅의 additionalContext·systemMessage·stdout 이 10,000자를 넘으면 파일로 빼고 앞 2,000자만 +# 보여 줘요. 그 파일을 읽으라고 하지도 않아요. 그래서 훅이 먼저 그 아래로 줄여요. +# 상한은 바이트로 재요. 바이트 수는 글자 수보다 작을 수 없어서 로케일과 상관없이 글자 상한도 지켜져요. +# 기본은 GOAX_CONTEXT_MAX(8000). 넘으면 "… N바이트 생략 — <볼 곳>" 꼬리까지 합쳐 상한 안에 들어오게 자르고, +# 멀티바이트 글자 중간에서는 끊지 않아요. 자를 자리 가까이에 줄바꿈이 있으면 거기서 끊어요. +# 끝 줄바꿈은 지워져요 (`$(...)` 와 같아요). +goax_cap_context() { + local max="${1:-}" where="${2:-}" s n lines tail k cut cn pre pn drop i b need om ol + case "$max" in ''|*[!0-9]*) max="${GOAX_CONTEXT_MAX:-8000}" ;; esac + case "$max" in ''|*[!0-9]*) max=8000 ;; esac + s=$(cat) + n=$(printf '%s' "$s" | LC_ALL=C wc -c | tr -d ' ') + if [ "$n" -le "$max" ]; then printf '%s\n' "$s"; return 0; fi + lines=$(printf '%s\n' "$s" | wc -l | tr -d ' ') + # 꼬리 길이는 생략 수의 자릿수에 달려요 — 전체 크기로 위쪽 어림을 잡아요 + tail="… ${n}바이트·${lines}줄 생략${where:+ — $where}" + k=$(( max - $(printf '%s' "$tail" | LC_ALL=C wc -c | tr -d ' ') - 1 )) + [ "$k" -lt 0 ] && k=0 + # 파이프(`printf | head -c`)로 자르면 큰 입력에서 printf 가 SIGPIPE 로 "Broken pipe" 를 stderr 에 찍어요 — + # C 로케일 서브셸의 바이트 단위 부분 문자열로 잘라요 + cut=$(LC_ALL=C; printf '%s' "${s:0:$k}") + cn=$(printf '%s' "$cut" | LC_ALL=C wc -c | tr -d ' ') + pre="${cut%$'\n'*}" + if [ "$pre" != "$cut" ]; then + pn=$(printf '%s' "$pre" | LC_ALL=C wc -c | tr -d ' ') + [ $((pn * 4)) -ge $((cn * 3)) ] && { cut="$pre"; cn="$pn"; } + fi + # 끝에 반쯤 잘린 UTF-8 글자가 있으면 그 바이트들을 버려요 + # shellcheck disable=SC2046 + set -- $(printf '%s' "$cut" | LC_ALL=C tail -c 4 | od -An -tu1) + drop=0; i=$# + while [ "$i" -gt 0 ]; do + b="${!i}" + if [ "$b" -lt 128 ]; then break; fi + if [ "$b" -ge 192 ]; then + need=2; [ "$b" -ge 224 ] && need=3; [ "$b" -ge 240 ] && need=4 + [ $(( $# - i + 1 )) -lt "$need" ] && drop=$(( $# - i + 1 )) + break + fi + i=$((i - 1)) + done + if [ "$drop" -gt 0 ]; then + cn=$((cn - drop)) + cut=$(LC_ALL=C; printf '%s' "${cut:0:$cn}") + fi + om=$((n - cn)) + ol=$(( lines - $(printf '%s\n' "$cut" | wc -l | tr -d ' ') )) + [ "$ol" -lt 0 ] && ol=0 + if [ "$ol" -gt 0 ]; then tail="… ${om}바이트·${ol}줄 생략${where:+ — $where}" + else tail="… ${om}바이트 생략${where:+ — $where}"; fi + printf '%s\n%s\n' "$cut" "$tail" +} + # Exit codes EXIT_OK=0 EXIT_ERROR=1 @@ -1193,6 +1248,17 @@ goax_rule_contracts() { ' "$file" } +# ─── 작업별 상태 — .ax/tasks/.json ───────────────────────────── +# 작업 하나 = 파일 하나예요. current-task.json 은 **지금 작업**의 사본(최상위 task 필드)과 handoff 를 들고 +# 있어서, 읽는 쪽(HUD · 게이트 · triage)은 예전처럼 current-task.json 만 보면 돼요. 병렬 작업이면 각자 +# update-task.sh --task 로 자기 파일만 갱신하고, 지금 작업이 아닌 쪽은 current-task.json 을 건드리지 않아요. +# goax_task_key 파일 이름에 쓸 키 ([[:alnum:]_.-] 밖은 `_`) +# goax_task_path /.ax/tasks/.json +# GOAX_TASK_FIELDS 작업 필드 — 지금 작업을 바꿀 때 current-task.json 에서 지우고 새 작업 것으로 채워요 +GOAX_TASK_FIELDS='["task_id","description","size","risk","domain","spec_id","spec_dir","spec_tier","plan_doc","friction","started_at","updated_at","phase","intent_notes","blocked_by"]' +goax_task_key() { printf '%s' "${1:-}" | tr -c '[:alnum:]_.-' '_'; } +goax_task_path() { printf '%s/.ax/tasks/%s.json' "${1:-.}" "$(goax_task_key "${2:-}")"; } + # goax_glob_owners # stdin `

[--json] [--dry-run] -# bash update-task.sh [--phase

] [--set = ...] [--blocked-by ''] \ -# [--merge-intent ''] [--start] [--json] [--dry-run] +# bash update-task.sh [--task ] [--phase

] [--set = ...] [--blocked-by ''] \ +# [--merge-intent ''] [--start] [--activate] [--json] [--dry-run] # phase: triaged | spec | spec_checked | spec_blocked | tasks | implementing | review (idle 은 reset-task.sh 만) # --set 키: task_id · description · size(S|M|L|XL) · risk(L0|L1|L2|L3) · domain · # spec_id · spec_dir · spec_tier(standard|full) · plan_doc(외부 계획 문서 경로) · @@ -13,7 +13,14 @@ # spec-implement 가 config.yml 의 confirmation.mode 대신 이 값을 써요. L3 override(C5)는 여전히 우선해요. # --blocked-by blocked_by 를 통째로 교체 (문자열 JSON 배열, '[]' 로 비움) # --merge-intent intent_notes 에 얕은 병합 (JSON 객체) — 같은 키면 새 값이 우선해요 -# --start started_at 을 지금(UTC)으로 — triage 가 새 task 를 열 때만 +# --start started_at 을 지금(UTC)으로 — triage 가 새 task 를 열 때만. 새 task 가 지금 작업이 돼요 +# --task 이 작업만 갱신해요 (.ax/tasks/.json). 지금 작업(current-task.json 의 task_id)이 아니면 +# current-task.json 은 그대로예요 — 병렬 작업이 서로 덮지 않아요. 없으면 지금 작업이 대상 +# --activate --task 의 작업을 지금 작업으로 (current-task.json 의 task 필드를 그 작업 것으로 바꿔요) +# +# 작업별 상태: task_id 가 있는 작업은 .ax/tasks/.json 이 원본이고 current-task.json 은 지금 작업의 +# 사본 + handoff 예요. --start 로 다른 작업을 열면 앞 작업은 자기 파일에 남아요 (--task --activate 로 돌아가요). +# task_id 가 없는 옛 형식은 예전처럼 current-task.json 만 고쳐요. # updated_at 은 매번 지금(UTC)으로 찍어요. 인자가 하나도 없으면 exit 1. # # 왜 있나 — triage · spec · spec-validate · spec-tasks · spec-implement 가 SKILL.md 안의 인라인 jq 로 @@ -27,7 +34,8 @@ # 영영 막혀요. # # Output (--json): -# {"status":"ok","result":{"path":".ax/current-task.json","phase":"<갱신 후>","changed":["phase","updated_at",…]},…} +# {"status":"ok","result":{"path":".ax/current-task.json","phase":"<갱신 후>","changed":["phase","updated_at",…], +# "task_id":"<대상>"|null,"task_file":".ax/tasks/.json"|null,"active":},…} # Exit: 0 ok · 1 error (파일 없음 · 값 검증 실패 · 락 대기 초과 · JSON 깨짐) · 2 skipped (jq 없음) set -euo pipefail @@ -36,13 +44,13 @@ SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/common.sh" JSON_MODE=false; SHOW_HELP=false; DRY_RUN=false -PHASE=""; SETS='{}'; SET_KV=(); BLOCKED=""; INTENT=""; START=false; NARGS=0; EMPTY_OPT=""; opt="" +PHASE=""; SETS='{}'; SET_KV=(); BLOCKED=""; INTENT=""; START=false; ACTIVATE=false; TASK_OPT=""; NARGS=0; EMPTY_OPT=""; opt="" while [ $# -gt 0 ]; do case "$1" in --json) JSON_MODE=true ;; --dry-run) DRY_RUN=true ;; --help|-h) SHOW_HELP=true ;; - --phase|--set|--blocked-by|--merge-intent) + --phase|--set|--blocked-by|--merge-intent|--task) opt="$1" # 값이 비었거나 다음 토큰이 옵션(--…)이면 값을 삼키지 않고 오류로 — 예전엔 빈 값을 "안 준 것" 으로 접어 # updated_at 만 조용히 쓰고 ok 였고 (spec-validate 가 BLOCKED 를 비워 보내면 phase 만 spec_blocked 로 @@ -56,10 +64,12 @@ while [ $# -gt 0 ]; do --set) SET_KV+=("$1") ;; --blocked-by) BLOCKED="$1" ;; --merge-intent) INTENT="$1" ;; + --task) TASK_OPT="$1"; NARGS=$((NARGS - 1)) ;; # 대상만 고르고 바꿀 건 아니에요 esac fi NARGS=$((NARGS + 1)) ;; --start) START=true; NARGS=$((NARGS + 1)) ;; + --activate) ACTIVATE=true; NARGS=$((NARGS + 1)) ;; *) goax_error "unknown option: $1"; exit "$EXIT_ERROR" ;; esac [ $# -gt 0 ] && shift @@ -76,7 +86,11 @@ if ! command -v jq >/dev/null 2>&1; then if [ "$JSON_MODE" = true ]; then json_skip "jq 가 필요해요 — current-task.json 은 JSON 이에요"; fi goax_warn "jq 가 없어 skip"; exit "$EXIT_SKIPPED" fi -[ "$NARGS" -gt 0 ] || fail "바꿀 것이 없어요 — --phase · --set · --blocked-by · --merge-intent · --start 중 하나는 줘요" +[ "$NARGS" -gt 0 ] || fail "바꿀 것이 없어요 — --phase · --set · --blocked-by · --merge-intent · --start · --activate 중 하나는 줘요" +[ "$ACTIVATE" = false ] || [ -n "$TASK_OPT" ] || fail "--activate 는 --task 와 같이 줘요 — 어느 작업을 지금 작업으로 할지요" +# 작업 id 는 파일 이름이 돼요 — 고치지 않고 거부해요 (`a/b` 와 `a_b` 가 같은 파일로 가면 안 돼요) +valid_id() { printf '%s' "$1" | grep -Eq '^[[:alnum:]_][[:alnum:]_.-]*$'; } +[ -z "$TASK_OPT" ] || valid_id "$TASK_OPT" || fail "작업 id 는 영숫자·_·.·- 만 써요 (받은 값: '${TASK_OPT}')" # ── 값 검증 — 전부 파일을 열기 전에. 하나라도 틀리면 아무것도 안 써요 ── # enum 은 case 로 잡되 [a-z] 범위는 안 써요 (en_US.UTF-8 에서 [a-z] 가 대문자도 물어요 — CLAUDE.md) @@ -96,7 +110,8 @@ for kv in ${SET_KV[@]+"${SET_KV[@]}"}; do risk) case "$v" in L0|L1|L2|L3) ;; *) fail "risk 는 L0|L1|L2|L3 중 하나예요 (받은 값: '${v}')" ;; esac ;; spec_tier) case "$v" in standard|full) ;; *) fail "spec_tier 는 standard|full 중 하나예요 (받은 값: '${v}')" ;; esac ;; friction) case "$v" in autopilot|phase_gate|per_task) ;; *) fail "friction 은 autopilot|phase_gate|per_task 중 하나예요 (받은 값: '${v}')" ;; esac ;; - task_id|description|domain|spec_id|spec_dir) ;; + task_id) valid_id "$v" || fail "task_id 는 영숫자·_·.·- 만 써요 (받은 값: '${v}')" ;; + description|domain|spec_id|spec_dir) ;; plan_doc) case "$v" in *$'\n'*) fail "plan_doc 는 경로 한 줄이에요" ;; esac ;; *) fail "--set 이 받는 키는 task_id·description·size·risk·domain·spec_id·spec_dir·spec_tier·friction·plan_doc 이에요 (받은 키: '${k}')" ;; esac @@ -132,27 +147,107 @@ FILTER='. + $sets | .updated_at = $ts' TS=$(date -u +%Y-%m-%dT%H:%M:%SZ) +# 대상 작업 고르기 — --task > (--start 의) 새 task_id > 지금 작업. 비면 옛 형식(current-task.json 만). +# dry-run 은 락 없이, 실제 쓰기는 락 안에서 불러요. 막을 일이면 ERRMSG 를 채우고 1. +TASK_TTL=$(( ${GOAX_TASK_TTL_DAYS:-7} * 86400 )) +resolve_target() { + ERRMSG=""; TF=""; TASK_REL="" + ACTIVE=$(jq -r '.task_id // empty' "$FILE") + NEW_ID=$(printf '%s' "$SETS" | jq -r '.task_id // empty') + TARGET="$TASK_OPT" + if [ -n "$TARGET" ] && [ -n "$NEW_ID" ] && [ "$NEW_ID" != "$TARGET" ]; then + ERRMSG="--task ${TARGET} 와 --set task_id=${NEW_ID} 가 달라요 — 작업 id 는 바꾸지 않아요"; return 1 + fi + [ -n "$TARGET" ] || TARGET="$NEW_ID" + if [ -z "$TARGET" ] && [ -n "$ACTIVE" ]; then + # --task 없이 "지금 작업" 을 고치려는데 진행 중 작업이 여럿이면 어느 세션 것인지 몰라요 — 덮지 않고 되물어요 + local others + others=$(for f in "$PROJECT_ROOT"/.ax/tasks/*.json; do [ -f "$f" ] || continue + jq -r --arg a "$ACTIVE" --argjson ttl "$TASK_TTL" \ + 'select((.phase // "idle") != "idle" and (.task_id // "") != $a + and (now - (((.updated_at // "") | try fromdateiso8601 catch 0))) < $ttl) | .task_id' "$f" 2>/dev/null + done | paste -sd, -) + if [ -n "$others" ]; then + ERRMSG="진행 중 작업이 여럿이에요 (지금 ${ACTIVE} · 다른 ${others}) — 내 작업 id 를 --task 로 줘요 (triage 가 알려준 id)"; return 1 + fi + TARGET="$ACTIVE" + fi + [ -n "$TARGET" ] || return 0 + TF=$(goax_task_path "$PROJECT_ROOT" "$TARGET"); TASK_REL=${TF#"$PROJECT_ROOT"/} + if [ "$START" = true ] && [ -f "$TF" ]; then + ERRMSG="작업 ${TARGET} 이 이미 있어요 ($TASK_REL) — 이어서 하려면 --task ${TARGET} (--activate), 새 작업이면 새 id 로 --start"; return 1 + fi + if [ ! -f "$TF" ] && [ "$TARGET" != "$ACTIVE" ] && [ "$START" != true ]; then + ERRMSG="작업 ${TARGET} 이 없어요 ($TASK_REL) — 새 작업은 triage 가 --start 로 열어요"; return 1 + fi + return 0 +} + if [ "$DRY_RUN" = true ]; then # 락도 파일도 안 건드려요 — 검증만 하고 "무엇이 바뀔지" 를 답해요 (JSON 이 깨졌으면 여기서도 알려요) jq -e . "$FILE" >/dev/null 2>&1 || fail "$REL 을 읽지 못했어요 — JSON 이 깨졌는지 봐요" - RES=$(jq -nc --arg p "$REL" --arg ph "$PHASE" --argjson ch "$CHANGED" '{path: $p, dry_run: true, phase: (if $ph != "" then $ph else null end), changed: $ch}') + resolve_target || fail "$ERRMSG" + RES=$(jq -nc --arg p "$REL" --arg ph "$PHASE" --argjson ch "$CHANGED" --arg t "$TARGET" --arg tf "$TASK_REL" \ + '{path: $p, dry_run: true, phase: (if $ph != "" then $ph else null end), changed: $ch, + task_id: (if $t != "" then $t else null end), task_file: (if $tf != "" then $tf else null end)}') [ "$JSON_MODE" = true ] && json_output "ok" "$RES" "dry-run — 안 썼어요" || goax_log "dry-run — $REL 안 썼어요 (바뀔 키: $(printf '%s' "$CHANGED" | jq -r 'join(", ")'))" exit "$EXIT_OK" fi # 락 — status-note.sh · tier-from-state.sh --reset 과 같은 문자열 (락 단위는 파일). 읽기부터 락 안이에요. +# 작업 파일(.ax/tasks/.json)도 쓰면 그 락을 **안쪽에서** 잡아요 — 순서를 늘 current-task → 작업 파일로 고정해 +# tier-from-state.sh --reset 과 서로 기다리다 멈추지 않게 해요. LOCK="$(goax_normalize_path "$PROJECT_ROOT/.ax/current-task.json" "$PROJECT_ROOT").lock" goax_lock "$LOCK" "${GOAX_LOCK_TIMEOUT:-10}" || fail "다른 프로세스가 $REL 을 쓰는 중이에요 — 잠시 뒤 다시 해요" +jq -e . "$FILE" >/dev/null 2>&1 || { goax_unlock "$LOCK"; fail "$REL 을 읽지 못했어요 — JSON 이 깨졌는지 봐요 (원본은 그대로예요)"; } +resolve_target || { goax_unlock "$LOCK"; fail "$ERRMSG"; } +ARGS_JQ=(--arg p "$PHASE" --argjson sets "$SETS" --argjson bb "$BLOCKED" --argjson it "$INTENT" --argjson st "$START" --arg ts "$TS") TMP="$FILE.tmp.$$" -if ! jq --arg p "$PHASE" --argjson sets "$SETS" --argjson bb "$BLOCKED" --argjson it "$INTENT" --argjson st "$START" --arg ts "$TS" \ - "$FILTER" "$FILE" > "$TMP" 2>/dev/null; then - rm -f "$TMP"; fail "$REL 을 읽지 못했어요 — JSON 이 깨졌는지 봐요 (원본은 그대로예요)" +MIRROR=true; TASK_REL="" +if [ -z "$TARGET" ]; then + if ! jq "${ARGS_JQ[@]}" "$FILTER" "$FILE" > "$TMP" 2>/dev/null; then + rm -f "$TMP"; goax_unlock "$LOCK"; fail "$REL 을 읽지 못했어요 — JSON 이 깨졌는지 봐요 (원본은 그대로예요)" + fi + mv "$TMP" "$FILE" +else + mkdir -p "$(dirname "$TF")" || { goax_unlock "$LOCK"; fail ".ax/tasks/ 를 만들지 못했어요 — 권한을 봐요"; } + # --start 로 다른 작업을 열 때 지금 작업이 아직 파일이 없으면(옛 형식에서 넘어온 첫 갱신) 먼저 자기 파일로 남겨요 + if { [ "$START" = true ] || [ "$ACTIVATE" = true ]; } && [ -n "$ACTIVE" ] && [ "$ACTIVE" != "$TARGET" ]; then + AF=$(goax_task_path "$PROJECT_ROOT" "$ACTIVE") + if [ ! -f "$AF" ] && [ "$(jq -r '.phase // "idle"' "$FILE")" != idle ]; then + jq --argjson k "$GOAX_TASK_FIELDS" 'with_entries(select(.key as $x | $k | index($x)))' "$FILE" > "$AF" 2>/dev/null || rm -f "$AF" + fi + fi + TLOCK="$(goax_normalize_path "$TF" "$PROJECT_ROOT").lock" + goax_lock "$TLOCK" "${GOAX_LOCK_TIMEOUT:-10}" || { goax_unlock "$LOCK"; fail "다른 프로세스가 $TASK_REL 을 쓰는 중이에요 — 잠시 뒤 다시 해요"; } + if [ -f "$TF" ] && [ "$START" != true ]; then + BASE=$(jq -c . "$TF" 2>/dev/null) || { goax_unlock "$TLOCK"; goax_unlock "$LOCK"; fail "$TASK_REL 을 읽지 못했어요 — JSON 이 깨졌는지 봐요"; } + elif [ "$TARGET" = "$ACTIVE" ]; then + BASE=$(jq -c --argjson k "$GOAX_TASK_FIELDS" 'with_entries(select(.key as $x | $k | index($x)))' "$FILE") # 옛 형식 → 첫 갱신에 옮겨요 + else + BASE='{"intent_notes":{},"blocked_by":[]}' # --start 의 새 작업 (없는 작업은 resolve_target 이 이미 막았어요) + fi + REC=$(printf '%s' "$BASE" | jq -c "${ARGS_JQ[@]}" --arg id "$TARGET" "$FILTER | .task_id = \$id") \ + || { goax_unlock "$TLOCK"; goax_unlock "$LOCK"; fail "$TASK_REL 갱신 값을 만들지 못했어요"; } + printf '%s\n' "$REC" | jq . > "$TF.tmp.$$" && mv "$TF.tmp.$$" "$TF" \ + || { rm -f "$TF.tmp.$$"; goax_unlock "$TLOCK"; goax_unlock "$LOCK"; fail "$TASK_REL 을 쓰지 못했어요"; } + goax_unlock "$TLOCK" + # 지금 작업이거나 지금 작업이 되는 경우만 current-task.json 을 고쳐요 — 아니면 다른 세션의 지금 작업을 덮어요 + if [ "$TARGET" = "$ACTIVE" ] || [ "$START" = true ] || [ "$ACTIVATE" = true ]; then + if ! jq --argjson k "$GOAX_TASK_FIELDS" --argjson r "$REC" 'with_entries(select(.key as $x | ($k | index($x)) == null)) + $r' "$FILE" > "$TMP" 2>/dev/null; then + rm -f "$TMP"; goax_unlock "$LOCK"; fail "$REL 을 읽지 못했어요 — JSON 이 깨졌는지 봐요 (원본은 그대로예요)" + fi + mv "$TMP" "$FILE" + else + MIRROR=false + fi fi -mv "$TMP" "$FILE" -NEW_PHASE=$(jq -r '.phase // "idle"' "$FILE") +if [ "$MIRROR" = true ]; then NEW_PHASE=$(jq -r '.phase // "idle"' "$FILE"); else NEW_PHASE=$(jq -r '.phase // "idle"' "$TF"); fi goax_unlock "$LOCK" -RES=$(jq -nc --arg p "$REL" --arg ph "$NEW_PHASE" --argjson ch "$CHANGED" '{path: $p, phase: $ph, changed: $ch}') +RES=$(jq -nc --arg p "$REL" --arg ph "$NEW_PHASE" --argjson ch "$CHANGED" --arg t "$TARGET" --arg tf "$TASK_REL" --argjson a "$MIRROR" \ + '{path: $p, phase: $ph, changed: $ch, task_id: (if $t != "" then $t else null end), task_file: (if $tf != "" then $tf else null end), active: $a}') NEXT="phase=${NEW_PHASE}"; [ -n "$PHASE" ] && NEXT="phase → ${PHASE}" -[ "$JSON_MODE" = true ] && json_output "ok" "$RES" "$NEXT" || goax_log "$REL 갱신 — $(printf '%s' "$CHANGED" | jq -r 'join(", ")') ($NEXT)" +[ "$MIRROR" = true ] || NEXT="${NEXT} (작업 ${TARGET} 만 — 지금 작업은 ${ACTIVE:-없음} 그대로)" +[ "$JSON_MODE" = true ] && json_output "ok" "$RES" "$NEXT" || goax_log "${TASK_REL:-$REL} 갱신 — $(printf '%s' "$CHANGED" | jq -r 'join(", ")') ($NEXT)" exit "$EXIT_OK" diff --git a/templates/default/.gitignore.template b/templates/default/.gitignore.template index 8a005d0..7c671b4 100644 --- a/templates/default/.gitignore.template +++ b/templates/default/.gitignore.template @@ -1,6 +1,7 @@ # goax — runtime/임시 산출물 (per-machine, per-session) .ax/state.json .ax/current-task.json +.ax/tasks/ # 재생성 캐시 (build-memory.sh — markdown·상태에서 결정론 재생성) .ax/MEMORY.md diff --git a/tests/smoke.sh b/tests/smoke.sh index 5319771..0866089 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -2384,7 +2384,7 @@ grep -q -- '--delta' "$REPO/skills/spec-validate/SKILL.md" && grep -q -- '--fixu && pass "spec-validate·spec-tasks·agents — --delta/--fixup/--stage tasks 가 호출부·수신부에 모두 있음" || fail "spec-review 새 옵션 — 스크립트만 있고 skill/agent 가 안 씀" grep -q '빌드·테스트' "$REPO/agents/architect.md" && grep -q '빌드·테스트' "$REPO/agents/evaluator.md" && grep -q '검증 예산' "$REPO/skills/spec-validate/SKILL.md" \ && pass "spec 리뷰 검증 예산 — grep·read 만, 빌드·테스트 금지 (agents + skill)" || fail "spec 리뷰 — 검증 예산 미명시" -grep -q 'update-task.sh --phase implementing' "$REPO/skills/spec-implement/SKILL.md" && grep -q 'update-task.sh --phase review' "$REPO/skills/spec-implement/SKILL.md" \ +grep -qE 'update-task\.sh .*--phase implementing' "$REPO/skills/spec-implement/SKILL.md" && grep -qE 'update-task\.sh .*--phase review' "$REPO/skills/spec-implement/SKILL.md" \ && pass "spec-implement — phase implementing/review 를 실제로 씀 (HUD 가 움직이는 조건)" || fail "spec-implement — phase 기록 없음 (HUD 가 tasks 에 멈춤)" grep -q 'hud' "$REPO/templates/default/.ax/hud/state.json.template" && grep -q 'hud.plugin_version' "$REPO/docs/state-ownership.md" \ && pass "state.json hud 캐시 — template · ownership 문서 동기화" || fail "state.json hud 캐시 3-way 동기화 누락" @@ -5040,6 +5040,173 @@ VL_MANY=$(CLAUDE_PROJECT_DIR="$VL" bash .ax/hooks/pre-commit/critical-rule-grep. popd >/dev/null || true rm -rf "$VL" +# ─────────────────────────────────────────────────────────── +section "61. 작업별 상태 — .ax/tasks/.json, 병렬 작업이 서로 덮지 않아요" +# ─────────────────────────────────────────────────────────── +PT=$(mktemp -d) +mkdir -p "$PT/.ax/scripts/bash" "$PT/.ax/hooks/pre-commit" "$PT/.ax/docs/spec/sa" "$PT/.ax/docs/spec/sb" "$PT/src" +cp "$REPO/templates/default/.ax/scripts/bash/"{common,update-task,tier-from-state,reset-task,session-brief,tasks-gate}.sh "$PT/.ax/scripts/bash/" +cp "$REPO/templates/default/.ax/hooks/pre-commit/spec-completion-gate.sh" "$PT/.ax/hooks/pre-commit/" +cp "$REPO/templates/default/.ax/current-task.json.template" "$PT/.ax/current-task.json" +echo '{}' > "$PT/.ax/state.json" +pt_u() { GOAX_PROJECT_DIR="$PT" bash "$PT/.ax/scripts/bash/update-task.sh" "$@" --json 2>/dev/null; } +pt_r() { GOAX_PROJECT_DIR="$PT" bash "$PT/.ax/scripts/bash/reset-task.sh" "$@" --json 2>/dev/null; } +pt_c() { jq -r "$1" "$PT/.ax/current-task.json"; } +pt_u --start --phase triaged --set task_id=A --set description=결제 >/dev/null +pt_u --phase implementing --set spec_dir=.ax/docs/spec/sa >/dev/null +pt_u --start --phase triaged --set task_id=B --set description=검색 >/dev/null +[ "$(pt_c .task_id)" = B ] && [ "$(jq -r '.phase + " " + .spec_dir' "$PT/.ax/tasks/A.json")" = "implementing .ax/docs/spec/sa" ] \ + && pass "update-task --start — 새 작업이 지금 작업이 되고 앞 작업은 .ax/tasks/A.json 에 남아요" \ + || fail "update-task --start — 앞 작업 유실: $(cat "$PT/.ax/current-task.json" | jq -c '{task_id,phase}')" +PTJ=$(pt_u --task A --phase review) +{ [ "$(printf '%s' "$PTJ" | jq -r '.result.active')" = false ] && [ "$(pt_c '.task_id + " " + .phase')" = "B triaged" ] \ + && [ "$(jq -r .phase "$PT/.ax/tasks/A.json")" = review ] && [ "$(pt_c '.handoff | type')" = object ]; } \ + && pass "update-task --task A — 지금 작업(B)·handoff 는 그대로, A 파일만 갱신" \ + || fail "update-task --task — 다른 작업을 덮음: $PTJ" +pt_u --task Z --phase review >/dev/null; [ $? -eq 1 ] && [ ! -f "$PT/.ax/tasks/Z.json" ] \ + && pass "update-task --task <없는 id> — exit 1 · 파일을 만들지 않아요" || fail "update-task — 없는 작업을 만들었어요" +# 진행 중 작업이 여럿인데 --task 가 없으면 어느 세션 것인지 몰라요 — 덮지 않고 되물어요 +PTN=$(pt_u --phase implementing); [ "$(printf '%s' "$PTN" | jq -r .status)" = error ] && [ "$(pt_c .phase)" = triaged ] \ + && pass "update-task — 진행 중 작업이 여럿이면 --task 없는 갱신은 거부 (지금 작업 그대로)" || fail "update-task — --task 없이 덮었어요: $PTN" +pt_u --start --set task_id=A >/dev/null; [ "$(jq -r .description "$PT/.ax/tasks/A.json")" = 결제 ] && [ "$(pt_c .task_id)" = B ] \ + && pass "update-task --start — 이미 있는 id 는 거부 (옛 기록 위에 병합하지 않아요)" || fail "update-task --start — 같은 id 로 덮었어요" +[ "$(pt_u --task 'a/b' --phase spec | jq -r .status)" = error ] && [ "$(pt_u --task NOPE --phase spec --dry-run | jq -r .status)" = error ] \ + && pass "update-task — 잘못된 id 거부 · --dry-run 도 없는 작업은 error" || fail "update-task — id 검증/dry-run 대상 확인 실패" +pt_u --task B --set spec_dir=.ax/docs/spec/sb --phase implementing >/dev/null +SB_OUT=$(GOAX_PROJECT_DIR="$PT" bash "$PT/.ax/scripts/bash/session-brief.sh" --json 2>/dev/null) +printf '%s' "$SB_OUT" | jq -e '(.result.other_tasks | map(.task_id)) == ["A"] and (.result.lines | map(select(test("다른 진행 중 작업"))) | length) == 1' >/dev/null \ + && pass "session-brief — 병렬 작업(A)을 다른 진행 중 작업으로 알려요" || fail "session-brief — other_tasks 누락: $(printf '%s' "$SB_OUT" | jq -c .result.other_tasks)" +# 완료 게이트는 진행 중 작업 전부의 spec 을 봐요 — 지금 작업이 B 여도 A 의 spec 을 건드리는 커밋은 걸려요 +printf '## 3. \n- [ ] **AC1** a\n' > "$PT/.ax/docs/spec/sa/spec.md"; printf -- '- [ ] T001 [AC1] a — files: src/a.ts\n' > "$PT/.ax/docs/spec/sa/tasks.md" +printf '## 3. \n- [ ] **AC1** a\n' > "$PT/.ax/docs/spec/sb/spec.md"; printf -- '- [ ] T001 [AC1] b — files: src/b.ts\n' > "$PT/.ax/docs/spec/sb/tasks.md" +echo x > "$PT/src/a.ts"; git -C "$PT" init -q 2>/dev/null; git -C "$PT" add src/a.ts 2>/dev/null +PG_OUT=$(cd "$PT" && CLAUDE_PROJECT_DIR="$PT" bash "$PT/.ax/hooks/pre-commit/spec-completion-gate.sh" 2>&1) +{ printf '%s' "$PG_OUT" | grep -q 'spec sa — 0/1 완료' && printf '%s' "$PG_OUT" | grep -q 'spec sb 미완료 1 — 이번 커밋과 무관해 건너뛰어요'; } \ + && pass "spec-completion-gate — 병렬 작업의 spec 도 봐요 (A 의 spec 은 자세히 · B 는 무관해 한 줄)" \ + || fail "spec-completion-gate — 병렬 작업 spec 판정 실패: $PG_OUT" +# G6 — 지금 작업이 아닌 spec 도 그 작업의 size×risk 로 evaluator 필수를 판정해요 +pt_u --task A --set size=L --set risk=L2 >/dev/null +GOAX_PROJECT_DIR="$PT" bash "$PT/.ax/scripts/bash/tasks-gate.sh" --spec sa --json 2>/dev/null | jq -e '.result.review_required == true' >/dev/null \ + && pass "tasks-gate G6 — 병렬 작업(지금 작업 아님)의 spec 도 evaluator 필수 판정" || fail "tasks-gate G6 — 비활성 작업 spec 의 필수 판정이 빠짐" +# 오래 멈춘 작업 — 완료 게이트는 건너뛰고 session-brief 는 정리하라고 알려요 +PG_OLD=$(cd "$PT" && GOAX_TASK_TTL_DAYS=0 CLAUDE_PROJECT_DIR="$PT" bash "$PT/.ax/hooks/pre-commit/spec-completion-gate.sh" 2>&1) +SB_OLD=$(GOAX_TASK_TTL_DAYS=0 GOAX_PROJECT_DIR="$PT" bash "$PT/.ax/scripts/bash/session-brief.sh" 2>/dev/null) +{ ! printf '%s' "$PG_OLD" | grep -q 'spec sa' && printf '%s' "$SB_OLD" | grep -q '오래 멈춘 작업: A'; } \ + && pass "TTL — 오래 멈춘 병렬 작업은 완료 게이트에서 빠지고 session-brief 가 정리를 권해요" || fail "TTL 처리 실패: $PG_OLD / $SB_OLD" +pt_u --task A --activate >/dev/null +[ "$(pt_c '.task_id + " " + .phase + " " + .description')" = "A review 결제" ] && [ "$(jq -r .phase "$PT/.ax/tasks/B.json")" = implementing ] \ + && pass "update-task --task A --activate — A 로 돌아가고 B 는 자기 파일에" || fail "update-task --activate 실패: $(pt_c '{task_id,phase}|tostring')" +[ "$(pt_r --task --json | jq -r .status 2>/dev/null)" != ok ] && [ -f "$PT/.ax/tasks/B.json" ] \ + && pass "reset-task --task 값 없음 — 다음 옵션을 id 로 삼키지 않아요" || fail "reset-task --task 가 --json 을 삼켰어요" +PR_J=$(pt_r --task B) +{ [ "$(printf '%s' "$PR_J" | jq -r '.result.active')" = false ] && [ ! -f "$PT/.ax/tasks/B.json" ] && [ "$(pt_c .task_id)" = A ]; } \ + && pass "reset-task --task B — B 파일만 지우고 지금 작업(A)은 그대로" || fail "reset-task --task — $PR_J" +pt_r >/dev/null +{ [ "$(pt_c .phase)" = idle ] && [ ! -f "$PT/.ax/tasks/A.json" ] && [ "$(pt_c '.handoff | type')" = object ]; } \ + && pass "reset-task — 지금 작업을 끝내면 파일도 지우고 idle · handoff 는 남아요" || fail "reset-task — 지금 작업 리셋 실패" +# 옛 형식 — task_id 가 있는 current-task.json 만 있고 작업 파일이 없으면 첫 갱신에 옮겨요 +jq '.task_id = "OLD" | .phase = "spec" | .description = "옛"' "$PT/.ax/current-task.json" > "$PT/ct.tmp" && mv "$PT/ct.tmp" "$PT/.ax/current-task.json" +pt_u --phase tasks >/dev/null +[ "$(jq -r '.description + " " + .phase' "$PT/.ax/tasks/OLD.json" 2>/dev/null)" = "옛 tasks" ] && [ "$(pt_c .phase)" = tasks ] \ + && pass "update-task — 옛 형식(작업 파일 없음)은 첫 갱신에 .ax/tasks/ 로 옮겨요" || fail "update-task — 옛 형식 이전 실패" +grep -qxF '.ax/tasks/' "$REPO/templates/default/.gitignore.template" \ + && pass ".gitignore.template — .ax/tasks/ (런타임 상태)" || fail ".gitignore.template — .ax/tasks/ 누락" +rm -rf "$PT" +section "62. 훅 출력 예산 — 모델에게 주는 글은 10,000자 상한 아래, 한글 중간에서 안 끊어요" +# ─────────────────────────────────────────────────────────── +# Claude Code 는 훅의 additionalContext·stdout 이 10,000자를 넘으면 파일로 빼고 앞 2,000자만 보여 줘요. +# 그 파일을 읽으라고 하지도 않아요. 그래서 주입 훅은 goax_cap_context 로 먼저 줄여요. +# 최악 입력(50KB 출력 lint · 모듈/룰 수백 개 · 아주 긴 브리핑)을 넣고, 출력이 유효한 JSON 이고 상한 아래인지 봐요. +if command -v jq >/dev/null 2>&1; then + OB=$(mktemp -d) + mkdir -p "$OB/.ax/scripts/bash" "$OB/.ax/hooks" "$OB/src" "$OB/.ax/spirit/rules" "$OB/.ax/modules" + cp "$REPO/templates/default/.ax/scripts/bash/"*.sh "$OB/.ax/scripts/bash/"; cp -R "$REPO/templates/default/.ax/hooks/"* "$OB/.ax/hooks/" + cp "$REPO/templates/default/.ax/config.yml" "$OB/.ax/config.yml"; cp "$REPO/templates/default/.ax/current-task.json.template" "$OB/.ax/current-task.json" + : > "$OB/src/a.ts" + # 상한 검사 — 유효 JSON · 문자 수 < 10000 · 바이트 수 ≤ 8000 · 깨진 글자(U+FFFD) 없음 · 생략 꼬리 있음 + ob_check() { # <이름> <훅 stdout> + local name="$1" out="$2" ctx chars bytes + ctx=$(printf '%s' "$out" | jq -er '.hookSpecificOutput.additionalContext' 2>/dev/null) \ + || { fail "$name — 유효한 JSON additionalContext 가 아니에요: $(printf '%s' "$out" | head -c 200)"; return; } + chars=$(printf '%s' "$out" | jq -r '.hookSpecificOutput.additionalContext | length') + bytes=$(printf '%s' "$ctx" | LC_ALL=C wc -c | tr -d ' ') + if [ "$chars" -lt 10000 ] && [ "$bytes" -le 8000 ] && ! printf '%s' "$ctx" | grep -q $'\xef\xbf\xbd' \ + && printf '%s' "$ctx" | grep -q '생략'; then + pass "$name — 최악 입력에서도 ${chars}자·${bytes}바이트, 유효 JSON, 생략 꼬리" + else + fail "$name — chars=$chars bytes=$bytes (상한 10000자·8000바이트, 생략 꼬리 필요)" + fi + } + # 헬퍼 단위 — 상한 아래면 그대로, 넘으면 꼬리까지 상한 안 · 바이트 경계가 글자 중간이어도 유효한 UTF-8 + OBH=$( (source "$REPO/templates/default/.ax/scripts/bash/common.sh" + [ "$(printf 'short' | goax_cap_context)" = short ] || echo "짧은 입력이 바뀜" + for m in 100 101 102 103; do + r=$(awk 'BEGIN{for(i=0;i<20000;i++) printf "가나다"; print ""}' | goax_cap_context "$m" "전체: x") + b=$(printf '%s' "$r" | LC_ALL=C wc -c | tr -d ' ') + [ "$b" -le "$m" ] || echo "max=$m 인데 $b바이트" + printf '%s' "$r" | jq -Rrs . 2>/dev/null | grep -q $'\xef\xbf\xbd' && echo "max=$m 에서 글자가 깨짐" + printf '%s' "$r" | tail -1 | grep -q '바이트 생략 — 전체: x$' || echo "max=$m 꼬리 없음: $(printf '%s' "$r" | tail -1)" + done) 2>&1) + [ -z "$OBH" ] && pass "goax_cap_context — 상한 아래면 그대로, 넘으면 꼬리까지 상한 안 · 3바이트 글자를 반으로 안 잘라요" \ + || fail "goax_cap_context: $OBH" + + # lint-changed — 50KB 넘게 찍고 실패하는 lint_file 명령 + cat > "$OB/big-lint.sh" <<'EOF' +awk 'BEGIN{for(i=0;i<20000;i++) printf "한글"; print ""; for(i=0;i<500;i++) print "src/a.ts:" i " 경고 " i}' +exit 1 +EOF + (cd "$OB" && bash .ax/scripts/bash/config-set.sh --add commands.lint_file '**/*.ts => bash big-lint.sh {file}' >/dev/null) + ob_check "lint-changed" "$(printf '{"tool_name":"Edit","tool_input":{"file_path":"%s"}}' "$OB/src/a.ts" \ + | CLAUDE_PROJECT_DIR="$OB" bash "$OB/.ax/hooks/post-edit/lint-changed.sh")" + + # module-rules-inject — src/** 에 걸린 모듈 300개 + i=0; while [ "$i" -lt 300 ]; do + mkdir -p "$OB/.ax/modules/module-with-a-fairly-long-name-$i" + printf -- '---\napplies_to: [code]\npaths:\n - "src/**"\n---\n' > "$OB/.ax/modules/module-with-a-fairly-long-name-$i/rules.md" + printf -- '---\ncategory: c%s\npaths:\n - "src/**"\n---\n## SP-C%s-001: x\n' "$i" "$i" > "$OB/.ax/spirit/rules/category-with-a-long-name-$i.md" + i=$((i + 1)) + done + ob_check "module-rules-inject" "$(printf '{"tool_name":"Edit","tool_input":{"file_path":"src/a.ts"}}' \ + | CLAUDE_PROJECT_DIR="$OB" bash "$OB/.ax/hooks/pre-edit/module-rules-inject.sh")" + ob_check "spirit-rules-inject" "$(printf '{"tool_name":"Edit","tool_input":{"file_path":"src/a.ts"}}' \ + | CLAUDE_PROJECT_DIR="$OB" bash "$OB/.ax/hooks/pre-edit/spirit-rules-inject.sh")" + + # session-brief 훅 — 스크립트 상한을 아주 크게 잡은 경우를 흉내 내요 (스크립트를 50KB 를 내는 가짜로) + cat > "$OB/.ax/scripts/bash/session-brief.sh" <<'EOF' +awk 'BEGIN{printf "{\"status\":\"ok\",\"result\":{\"lines\":["; for(i=0;i<1000;i++) printf "%s\"인계 노트 항목 %d — 아주 긴 설명이 붙어 있어요\"", (i?",":""), i; print "]}}"}' +EOF + ob_check "session-brief 훅" "$(printf '{"session_id":"s","source":"startup"}' \ + | CLAUDE_PROJECT_DIR="$OB" bash "$OB/.ax/hooks/session-start/session-brief.sh")" + + # harness-pointer — 경로 몇 줄이라 자를 일은 없지만, 출력은 유효 JSON 이고 같은 함수를 거쳐요 + HPO=$(printf '{"agent_type":"Explore"}' | CLAUDE_PROJECT_DIR="$OB" bash "$OB/.ax/hooks/subagent-start/harness-pointer.sh") + { [ -z "$HPO" ] || printf '%s' "$HPO" | jq -e '.hookSpecificOutput.additionalContext | length < 10000' >/dev/null; } \ + && pass "harness-pointer — 유효 JSON · 상한 아래" || fail "harness-pointer 출력: $HPO" + + # block-destructive — heredoc 으로 50KB 를 쓰는 명령 끝에 rm -rf / 가 붙으면 되돌려 주는 원문은 앞부분만 + BIG=$(awk 'BEGIN{for(i=0;i<20000;i++) printf "가나"}') + BDE=$(jq -nc --arg c "cat > f <<'X' +$BIG +X +rm -rf /" '{tool_input:{command:$c}}' | CLAUDE_PROJECT_DIR="$OB" bash "$OB/.ax/hooks/pre-bash/block-destructive.sh" 2>&1 >/dev/null); BDR=$? + BDB=$(printf '%s' "$BDE" | LC_ALL=C wc -c | tr -d ' ') + [ "$BDR" = 2 ] && [ "$BDB" -lt 2000 ] && printf '%s' "$BDE" | grep -q '생략 — 원문은 방금 보낸 명령 그대로예요' \ + && pass "block-destructive — 막을 때 되돌려 주는 명령은 앞 600바이트만 (${BDB}바이트)" \ + || fail "block-destructive 큰 명령: rc=$BDR bytes=$BDB" + rm -rf "$OB" +else + pass "§61 — jq 없음, skip" +fi +# 정적 — additionalContext 를 내는 훅은 전부 goax_cap_context 를 거쳐요 (stop/ 은 다른 레인이 맡아요) +OB_MISS="" +for h in "$REPO"/templates/default/.ax/hooks/*/*.sh; do + case "$h" in */stop/*|*/pre-commit/*) continue ;; esac + grep -q 'additionalContext:' "$h" || continue + grep -q 'goax_cap_context' "$h" || OB_MISS="$OB_MISS $(basename "$(dirname "$h")")/$(basename "$h")" +done +[ -z "$OB_MISS" ] && pass "additionalContext 를 내는 훅 전부 goax_cap_context 를 거쳐요" \ + || fail "goax_cap_context 없이 additionalContext 를 내는 훅:$OB_MISS" + # ─────────────────────────────────────────────────────────── section "✨ 결과" # ───────────────────────────────────────────────────────────