From 70c19cbf1c45f5dea3326b582880e6d5664c123f Mon Sep 17 00:00:00 2001 From: stone Date: Tue, 6 Oct 2026 15:55:32 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix(harness):=20=EB=8B=A4=EC=9D=8C=20task?= =?UTF-8?q?=20=EA=B0=80=20=EC=9D=98=EC=A1=B4=EC=9D=84=20=EB=B4=90=EC=9A=94?= =?UTF-8?q?=20=C2=B7=20=EC=99=84=EB=A3=8C=20=EC=A0=84=EC=97=90=20=EB=A3=B0?= =?UTF-8?q?=EC=9D=84=20=EB=8C=80=EC=A1=B0=ED=95=B4=EC=9A=94=20=C2=B7=20?= =?UTF-8?q?=EB=81=9D=EB=82=9C=20spec=20=EC=97=90=20=ED=9B=84=EC=86=8D?= =?UTF-8?q?=EC=9D=84=20=EC=9D=B4=EC=96=B4=20=EC=97=B4=EC=96=B4=EC=9A=94=20?= =?UTF-8?q?=C2=B7=20=ED=95=A9=EC=9D=98=20=EB=A6=AC=EB=B7=B0=20=EC=83=81?= =?UTF-8?q?=ED=95=9C=EC=97=90=EC=84=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20(v0.7.10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commerce 레포 상품 별점 원장 작업(spec 2026-10-06-c627)에서 겪은 마찰 다섯 가지. - mark-task --next 가 의존: 을 지켜요 (tasks-plan 과 같은 파서 goax_tasks_parse) - spec-tasks·레인·위임 브리프에 관례 위치 grep, files: 와 충돌하면 멈추고 보고 - 완료 게이트 G7 — rules-auditor 가 변경 파일별 룰·이웃 관례를 대조해 review-rules.md 에 verdict - update-task.sh --start --follow-up 으로 끝난 spec 에 후속 작업 - spec-review.sh --override --reason 으로 상한 도달 시 사용자 승인 기록 Co-Authored-By: Claude Opus 5.5 --- .claude-plugin/marketplace.json | 4 +- .claude-plugin/plugin.json | 2 +- CLAUDE.md | 10 +- CONCEPTS.md | 4 + README.ko.md | 2 +- README.md | 2 +- VERSION | 2 +- agents/evaluator.md | 6 + agents/lane-worker.md | 4 + agents/rules-auditor.md | 86 ++++++++++ changelog/0.7.10.md | 47 ++++++ changelog/README.md | 1 + skills/lane/SKILL.md | 6 +- skills/spec-implement/SKILL.md | 47 +++++- skills/spec-tasks/SKILL.md | 6 +- skills/spec-validate/SKILL.md | 10 ++ skills/triage/SKILL.md | 2 + skills/vendor/SKILL.md | 4 +- .../default/.ax/_templates/spec/tasks.md | 13 +- .../.ax/hooks/pre-edit/spirit-rules-inject.sh | 11 +- templates/default/.ax/hooks/stop/spec-gate.sh | 2 +- .../hooks/subagent-start/harness-pointer.sh | 2 +- templates/default/.ax/scripts/bash/README.md | 13 +- templates/default/.ax/scripts/bash/common.sh | 63 +++++++ .../default/.ax/scripts/bash/mark-task.sh | 64 +++++++- .../.ax/scripts/bash/rules-audit-scope.sh | 155 ++++++++++++++++++ .../default/.ax/scripts/bash/spec-review.sh | 68 +++++++- .../default/.ax/scripts/bash/tasks-gate.sh | 53 ++++-- .../default/.ax/scripts/bash/tasks-plan.sh | 57 +------ .../.ax/scripts/bash/tier-from-state.sh | 2 +- .../default/.ax/scripts/bash/update-task.sh | 27 ++- tests/smoke.sh | 135 ++++++++++++++- 32 files changed, 796 insertions(+), 114 deletions(-) create mode 100644 agents/rules-auditor.md create mode 100644 changelog/0.7.10.md create mode 100755 templates/default/.ax/scripts/bash/rules-audit-scope.sh diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 736ae9f..dedf3f0 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.9", + "version": "0.7.10", "author": { "name": "bluecheat", "email": "itsinil@gmail.com" @@ -30,5 +30,5 @@ ] } ], - "version": "0.7.9" + "version": "0.7.10" } diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 5dd60d6..8954572 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "goax", - "version": "0.7.9", + "version": "0.7.10", "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 ea14a43..ab4642c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,11 +22,11 @@ There is no `npm`/`make`/`gradle`. Anything more complex is invoked through Clau - `.claude-plugin/{plugin.json,marketplace.json}` — Claude Code plugin manifest. Both files plus `VERSION` must hold the **same version string** (smoke test fails otherwise). - `skills//SKILL.md` — 16 skills (`up`, `onboarding`, `zero`, `doctor`, `spec`, `spec-{tasks,implement,validate}`, `lane`, `screen`, `audit`, `mistake`, `hud`, `triage`, `adr`, `vendor`). Each starts with frontmatter `name:` + `description:` where description is the trigger-keyword bag. Note: `install` was renamed to `up` (0.1.18), `spec-plan` was retired (0.1.16), `mistake` was added as the capture-only counterpart to `audit`. **`zero` and `lane` were added in 0.5.0** (`lane` was born as `parallel` and renamed in 0.5.1 — the unit of the skill is the lane, and the agents `lane-*` and scripts `lanes-*` already carried that name) — `zero` is the 0→1 entry point (`up` §6 hands greenfield to it; `onboarding` stays brownfield-only), and `lane` sits between `spec-tasks` and `spec-implement` to decide whether the work can be split at all. **`rules` and `spirit` were retired in 0.5.3**: both were prose telling the model to grep; they became `rules-index.sh` and `spirit-lint.sh`, and `doctor` owns their triggers ("rules 보여줘", "spirit 점검"). Big skills keep `SKILL.md` under 500 lines and push box mockups / incident write-ups / mode-specific loops into `/references/*.md` (`onboarding`, `lane`, `spec-implement`, `triage`, `zero` do this) — `tests/smoke.sh` enforces the line cap, the 1,536-char description cap, and that every referenced `references/.md` exists. - `commands/goax.md` — single slash-command index that delegates to skills (other thin per-skill wrappers were retired). -- `agents/{architect,evaluator,lane-scout,lane-worker,screen-designer}.md` — sub-agents invoked by skills. `screen-designer` is the screen executor: the `screen` skill does intake (`design-caps.sh`), mode and spec-first tables, then spawns it in a fresh context to build → render → measure (`screen-measure.sh`) → fix (≤3) and write a report whose first line is `verdict: 통과|보강 필요|실측 아님`. The pair is **self-contained** — all design knowledge lives in `skills/screen/references/`; MCPs, browsers and simulators are optional accelerators, never requirements. `evaluator` is the Generator/Evaluator separation: `spec-implement` §8 spawns it in a fresh context and it writes `/review.md` whose first line is `verdict: 진행|보강 필요|재논의 필요`; `tasks-gate.sh` G6 reads that line. `lane-scout`/`lane-worker` are the read-only investigation lane and the file-ownership execution lane that `spec-implement` runs in lane mode. +- `agents/{architect,evaluator,lane-scout,lane-worker,rules-auditor,screen-designer}.md` — sub-agents invoked by skills. `rules-auditor` is the completion-time rules check: `spec-implement` §8.1.5 spawns it in a fresh context (in the same message as `evaluator`) with `rules-audit-scope.sh --json` (per changed file, the spirit/module rules matched by the **same** functions the edit hooks use, plus always-on rules and new files) and it writes `/review-rules.md` whose first line is `verdict: 진행|보강 필요`; `tasks-gate.sh` G7 reads it with G6's required-ness. It exists because the edit hooks only point at rule paths and `rule-read-gate.sh` only proves a Read — nobody checked compliance or neighbor conventions (new-file location/naming/split). `evaluator` explicitly does not do name/package convention checks. `screen-designer` is the screen executor: the `screen` skill does intake (`design-caps.sh`), mode and spec-first tables, then spawns it in a fresh context to build → render → measure (`screen-measure.sh`) → fix (≤3) and write a report whose first line is `verdict: 통과|보강 필요|실측 아님`. The pair is **self-contained** — all design knowledge lives in `skills/screen/references/`; MCPs, browsers and simulators are optional accelerators, never requirements. `evaluator` is the Generator/Evaluator separation: `spec-implement` §8 spawns it in a fresh context and it writes `/review.md` whose first line is `verdict: 진행|보강 필요|재논의 필요`; `tasks-gate.sh` G6 reads that line. `lane-scout`/`lane-worker` are the read-only investigation lane and the file-ownership execution lane that `spec-implement` runs in lane mode. - `scripts/provision.sh` — the installer. `up` calls it with `--json`; it reads MANIFEST and copies `templates/default/` into the user project. Lives at the plugin root (not `templates/default/.ax/scripts/`) because it *creates* `.ax/` — it cannot live inside what it installs. Root `install.sh` is an unrelated legacy stub that only prints install instructions. - `templates/default/` — **everything that gets copied into a user project** when the `up` skill runs. - `templates/default/MANIFEST` — installer SSOT. `up` reads this line-by-line; lines ending in `/` are recursive directory copies, `src -> dst` lines do rename copies. **When you add anything under `templates/default/`, update MANIFEST or the installer will not copy it.** Conditional copies (CLAUDE.md, settings.json, config.yml, mistakes/README.md, .gitignore) are intentionally outside MANIFEST and live in `skills/up/SKILL.md` §5–§6. -- `templates/default/.ax/scripts/bash/` — deterministic shell tooling (45 scripts). All follow the standard in `templates/default/.ax/scripts/bash/README.md`: `set -euo pipefail`, `--json --dry-run --help` options, `[goax]` stderr prefix, exit codes `0` ok / `1` error / `2` skipped. Their `--json` shape is `{status, result, next_step, warnings, errors}`. SKILL.md files invoke these and parse the JSON; they do not reimplement the logic inline. +- `templates/default/.ax/scripts/bash/` — deterministic shell tooling (46 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. 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. @@ -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. **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`). +- 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. `reset-task.sh` deletes the finished task's file, so a closed task cannot be `--activate`d; follow-up work on a finished spec opens a new task with `update-task.sh --start --follow-up --set task_id=`, which inherits `spec_id`/`spec_dir`/`spec_tier` from the spec directory (`.tier`) and records `intent_notes.follow_up_of`. 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. @@ -58,11 +58,11 @@ Three things must stay true together or the design breaks: - Any parser that walks `tasks.md` line-by-line (task counts, `[P]` conflict checks, ledger fields) must skip fenced code blocks first (`/^[[:space:]]*```/{fence=!fence; next} fence{next}`) — `_templates/spec/tasks.md` itself documents the task-line format inside a fence, and an unguarded parser counts that example as a real task. - New spec/ADR IDs are `YYYY-MM-DD-<4hex>` (`next-spec-num.sh --reserve --slug` creates the dir/file with O_EXCL), not sequential numbers — a sequence is "local max + 1", so parallel branches and PR stacks mint the same number (measured in a real project: 8 duplicated spec numbers, 12 duplicated ADR numbers). Legacy `NNN-slug/` / `NNNN-slug.md` stay readable and are never renamed (links would break); `--check-duplicates` reports legacy collisions only. The legacy ADR `0008-x` and a new `2026-09-25-…` both start with four digits and a dash, so decide the format with `GOAX_DOC_ID_ERE` / `goax_doc_key` / `goax_doc_sort` from `common.sh` — a parser that does `sub(/-.*/)` or `sed 's/^([0-9]+).*/'` collapses every new ID to `2026`. - The `${CLAUDE_SKILL_DIR}` env var is the official Claude Code variable for finding the plugin root (`${CLAUDE_SKILL_DIR}/../..`). `${CLAUDE_PLUGIN_ROOT}` exists only as a legacy fallback. Do not introduce other variable names. -- `spec-validate` runs the plan-time consensus review after the deterministic clarity gate: `spec-review.sh --snapshot` pins the spec sha, then `architect` and `evaluator` (spec mode) run **in parallel, in fresh contexts, never seeing each other's file** (independence comes from isolation, not ordering), each writing `review-spec..md` with a `verdict:` first line; `spec-review.sh --status` aggregates. The two review scopes are disjoint by contract — architect: structure/alternatives/reversal cost; evaluator: AC verifiability/code reality/missing edges/tasks — and each carries a `## 비차단` section that does not count toward the verdict; spec-stage reviewers grep and read only, never build or test. Required for size L/XL, optional (`--consensus`) for M, never for S — risk is deliberately not a factor (G6 already covers it). Do not let the main session write the reviewer files. The round counter counts reviews actually seen: `--snapshot` reuses the same sha, replaces an unreviewed snapshot in place, and only bumps after a reviewed one; `--stage tasks` restarts at 1 with cap 2 after `spec-tasks`; `--delta` writes the diff against the last reviewed copy (`.review-snapshot/`) for re-review briefs; `--fixup` accepts post-pass typo edits without a new round. `.review-round`, `.review-snapshot/` and the per-reviewer files are gitignored by the template — only the merged `review-spec.md` is committed. +- `spec-validate` runs the plan-time consensus review after the deterministic clarity gate: `spec-review.sh --snapshot` pins the spec sha, then `architect` and `evaluator` (spec mode) run **in parallel, in fresh contexts, never seeing each other's file** (independence comes from isolation, not ordering), each writing `review-spec..md` with a `verdict:` first line; `spec-review.sh --status` aggregates. The two review scopes are disjoint by contract — architect: structure/alternatives/reversal cost; evaluator: AC verifiability/code reality/missing edges/tasks — and each carries a `## 비차단` section that does not count toward the verdict; spec-stage reviewers grep and read only, never build or test. Required for size L/XL, optional (`--consensus`) for M, never for S — risk is deliberately not a factor (G6 already covers it). Do not let the main session write the reviewer files. The round counter counts reviews actually seen: `--snapshot` reuses the same sha, replaces an unreviewed snapshot in place, and only bumps after a reviewed one; `--stage tasks` restarts at 1 with cap 2 after `spec-tasks`; `--delta` writes the diff against the last reviewed copy (`.review-snapshot/`) for re-review briefs; `--fixup` accepts post-pass typo edits without a new round. `--override --reason ""` is the user-approval path once the stage's round cap is reached: it writes `review-override.md` (committed — sha · by · at · both verdicts · reason) and `--status` passes only while that sha equals the current one; before the cap it is refused. `.review-round`, `.review-snapshot/` and the per-reviewer files are gitignored by the template — only the merged `review-spec.md` is committed. - The HUD (`templates/default/.ax/hud/statusline.sh`) shows harness position only: `[goax#ver] | S×R · domain | spec › tasks › impl › review | mistakes:N`. It reads files only (no subprocess scripts) and depends on `current-task.json.phase` — `spec-implement` writes `implementing` at entry and `review` while waiting for the evaluator; without those writes the chain does not move. Heavier values live in `state.json.hud` written by `update-state.sh` (3-way sync with `state.json.template` and `docs/state-ownership.md`). - **Session handoff lives in `.ax/current-task.json` under `handoff`** (`now[]`·`now_at`·`next[]`·`open[]`·`renamed[]`), written only through `status-note.sh` (`--add|--done|--set `, 40-item cap, finished items are deleted — git log and ADRs are the SSOT for what got done); `now_at` is the freshness stamp the Stop gate reads, and `reset-task.sh` leaves `handoff` alone because `next`/`open`/`renamed` outlive a task. `triage` §1.0 reads it *before* `MEMORY.md`; `spec-implement` writes it on halt, on completion, and when a lane report renames a shared name; `zero` §13 opens it on day one. `MEMORY.md` (`build-memory.sh`) is a rule-token index, not a memory: even in lean mode it keeps the 🔴/🟡 pointers, and only the ADR/spec/module enumerations fold. There is no BM25 index any more (`build-index.sh` was removed in 0.5.3 — no project corpus ever crossed the threshold; bring it back only if `.ax/docs` measures past ~2,000 files). - `doctor` does not count anything inline. Its findings come from scripts: `doctor-scan.sh` (migration residue · hook registration against `settings.json.template` · Constitution-vs-installed mechanism · **reach map** — whether each rule source actually lands in a session: AGENTS.md without a `CLAUDE.md` that imports it reaches nothing), `spirit-lint.sh`, `rules-index.sh`, `check-rule-enforcement.sh`, `check-sensor-liveness.sh`. Add a new check as a script first, then a doctor section that reads its JSON. -- `spec-implement` is the only executor. It runs single-lane (sequential) unless `tasks.md` carries `레인:` ledger fields, in which case it is the coordinator: it stamps `디스패치:` via `lanes-dispatch.sh`, spawns `lane-worker` agents, stamps `보고:` when a report arrives, re-runs the task's `검증:` command itself, and only then flips the checkbox. Lanes never flip checkboxes and never write the ledger. Completion is `tasks-gate.sh` G1–G6; G5 is the ledger, G6 is the fresh-context `evaluator` verdict. The coordinator must not write `review.md` itself. When you add an execution edge (who invokes whom), wire it in the receiving SKILL.md, not just in the sender — the `lane`→`spec-implement` and triage-matrix→`evaluator` handoffs were prose-only until they were wired here. +- `spec-implement` is the only executor. It runs single-lane (sequential) unless `tasks.md` carries `레인:` ledger fields, in which case it is the coordinator: it stamps `디스패치:` via `lanes-dispatch.sh`, spawns `lane-worker` agents, stamps `보고:` when a report arrives, re-runs the task's `검증:` command itself, and only then flips the checkbox. Lanes never flip checkboxes and never write the ledger. Completion is `tasks-gate.sh` G1–G7; G5 is the ledger, G6 is the fresh-context `evaluator` verdict, G7 is the fresh-context `rules-auditor` verdict. `mark-task.sh --next` picks the first open task whose `의존:` are all `[x]`/`[~]` (same parser as `tasks-plan.sh`: `goax_tasks_parse` in `common.sh`) and returns `warning` + `blocked_tasks` when every open task is blocked — that is not "done", so it must not route to the completion gate. The coordinator must not write `review.md` itself. When you add an execution edge (who invokes whom), wire it in the receiving SKILL.md, not just in the sender — the `lane`→`spec-implement` and triage-matrix→`evaluator` handoffs were prose-only until they were wired here. ## Rule enforcement invariants diff --git a/CONCEPTS.md b/CONCEPTS.md index b9e8d24..bfb8416 100644 --- a/CONCEPTS.md +++ b/CONCEPTS.md @@ -257,6 +257,10 @@ Spirit: SP-- (예: SP-SEC-001) 써요. `tasks-gate.sh` G6 이 그 줄을 읽어 완료를 판정해요 — size L 이상 · M×L3 은 필수. 평가 기록을 구현 세션이 쓰지 않는 것, 그게 분리의 실체예요. 같은 이유로 병렬 레인의 완료도 레인의 자기보고가 아니라 코디네이터가 원장(`lanes-dispatch.sh`)에 `보고:` 를 찍고 검증 명령을 다시 돌린 뒤예요 (G5). +룰 대조도 같은 분리예요 — 편집 훅은 룰 경로를 알려주고 `rule-read-gate.sh` 는 읽었는지만 봐서, 읽은 룰을 지켰는지와 +새 파일이 이웃 관례(위치·이름·분리)를 따랐는지는 아무도 대조하지 않았어요. `rules-auditor` 가 새 컨텍스트로 +`rules-audit-scope.sh` 가 뽑은 범위(변경 파일마다 걸린 룰 · 상시 룰 · 새 파일)를 대조해 `review-rules.md` 첫 줄에 +`verdict:` 를 쓰고, G7 이 G6 과 같은 필수 여부로 읽어요. evaluator 는 이름·패키지 관례를 보지 않아요 — 범위가 겹치면 같은 지적이 두 번 와요. 같은 분리가 **계획 시점**에도 있어요 — `spec-validate` 가 명료성 게이트를 통과한 spec 에 대해 architect 와 evaluator 를 새 컨텍스트로 **병렬·독립**(서로 파일을 안 봄 — 독립은 순서가 아니라 격리에서 와요)으로 띄우고, 각자 `review-spec.<역할>.md` diff --git a/README.ko.md b/README.ko.md index 84d3781..25c490b 100644 --- a/README.ko.md +++ b/README.ko.md @@ -114,7 +114,7 @@ GOAX 가 깔아주는 자산은 두 결로 나뉘어요: "spec 만들어줘 payment-refund" → spec.md + tasks.md "tasks 분해" → tasks 분해 ([P] 병렬 마커) "구현 시작" → tasks.md 실행: 순차, 또는 레인 배정이 있으면 코디네이터 모드 - (lanes-dispatch.sh 원장) → 완료 게이트 G1~G6 + (lanes-dispatch.sh 원장) → 완료 게이트 G1~G7 (L · M×L3 은 새 컨텍스트 evaluator 필수) "레인 나눠줘" → lane — 가를 수 있는 일인지부터 판정 # 설계 결정은 ADR (.ax/docs/adr/NNNN-*.md) 로 diff --git a/README.md b/README.md index 7671c30..932f084 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ The `spec` skill doesn't generate every artifact upfront. The triage result deci "break into tasks" → tasks breakdown ([P] parallel marker); L/XL re-reviews with --stage tasks (cap 2) "start implementation" → run tasks.md: sequential, or as lane coordinator when tasks carry lane assignments (lanes-dispatch.sh ledger) - → completion gate G1–G6 (+ fresh-context evaluator for L / M×L3) + → completion gate G1–G7 (+ fresh-context evaluator and rules-auditor for L / M×L3) "split into lanes" → lane — first asks whether the work can be split at all "build this screen" → screen — spec tables first → screen-designer (fresh context) builds, renders, measures (screen-measure.sh) and fixes before showing diff --git a/VERSION b/VERSION index 972ef76..5b209ea 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.7.9 +0.7.10 diff --git a/agents/evaluator.md b/agents/evaluator.md index bbe7449..40d7bac 100644 --- a/agents/evaluator.md +++ b/agents/evaluator.md @@ -42,6 +42,12 @@ Generator/Evaluator 모델 — Generator의 self-praise bias 제거. - 컨텍스트 drift (거부된 패턴 재출현) - 사용자가 명시 안 했지만 도메인이 요구하는 것 +## 하지 않는 것 — 구현 리뷰 + +- **이름·패키지 관례 대조** — 변경 파일에 걸린 spirit·모듈 룰과 새 파일의 위치(`projection/`·`vo/`·`dto/` 같은 + 이웃 디렉토리)·이름 꼴·분리를 이웃과 맞춰 보는 건 `rules-auditor` 의 범위예요 (G7, `review-rules.md`). + 여기서도 하면 같은 지적이 두 파일에 와요. 보이면 `### 종합` 에 "룰 대조 범위" 로 한 줄만 적어요 + ## 호출 시점 - **`spec-implement` §8** — `tasks-gate.sh` 가 task 0건 남았다고 판정한 직후. size L 이상 · diff --git a/agents/lane-worker.md b/agents/lane-worker.md index 62c7c58..41dae96 100644 --- a/agents/lane-worker.md +++ b/agents/lane-worker.md @@ -34,6 +34,10 @@ description: "실행 레인 sub-agent — tasks.md 를 파일 소유권으로 - **소유 목록 밖 파일 편집** — 발견해서 고쳐야 할 게 있으면 고치지 말고 보고해요. 다른 세션이 그 파일을 이미 고쳤을 수 있고, 커밋 메시지는 새 안을 말하는데 바이트는 옛 안인 사고가 실제로 났어요 +- **소유 목록과 레포 관례가 충돌할 때 목록 안 파일에 끼워 넣기** — 새 타입을 만들어야 하는데 이웃 관례가 + 별도 파일·별도 위치(같은 레이어의 `projection/`·`vo/`·`dto/` 등)인데 그 경로가 소유 목록에 없으면, 소유 파일 + 안에 중첩 선언하지 말고 멈추고 보고해요 — `관례 충돌 — <새 타입>, 이웃 관례 <경로 2개>, 필요한 파일 <경로>`. + 목록을 늘리는 건 코디네이터가 해요. 끼워 넣으면 목록은 지켰지만 레포 관례를 어긴 코드가 남아요 - **커밋** — 워킹 트리에 변경만 남겨요. 합치는 건 한 사람이 해요 - **`git add -A` / `git add .`** — 동시 세션의 파일까지 쓸어담아요. 실제로 다른 세션 파일 3개가 딸려 들어간 적이 있어요. "워킹 트리는 내 것" 이라는 단독 점유 가정이 diff --git a/agents/rules-auditor.md b/agents/rules-auditor.md new file mode 100644 index 0000000..dbad140 --- /dev/null +++ b/agents/rules-auditor.md @@ -0,0 +1,86 @@ +--- +name: rules-auditor +description: "완료 직전 룰 대조 sub-agent — spec-implement §8 이 새 컨텍스트로 띄워요. 변경 파일마다 걸린 룰(rules-audit-scope.sh 결과)과 새로 생긴 심볼의 이웃 관례(위치·이름·분리)를 대조해 review-rules.md 첫 줄에 verdict 를 써요. 코드는 고치지 않아요. 트리거: '룰 대조', 'rules audit', '룰 리뷰', '관례 대조'." +--- + +# rules-auditor — 룰 대조 sub-agent + +## 시작 전 필수 + +`.ax/spirit/values.md`, `tone.md` 를 읽고 따라요. + +## 왜 따로 있나 + +편집 시점 훅은 룰 **경로**를 알려주고(`spirit-rules-inject.sh` · `module-rules-inject.sh`), `rule-read-gate.sh` 는 +그 파일을 **읽었는지**만 봐요. 읽은 룰을 지켰는지, 새 파일이 이웃 관례를 따랐는지는 아무도 대조하지 않았어요 — +`evaluator` 는 spec·ADR 적합성과 빠진 엣지를 보고, 이름·패키지 관례는 보지 않아요. 그래서 완료 게이트(G7)가 +이 리뷰를 따로 받아요. + +## 성립 조건 + +**1. 새 컨텍스트여야 해요.** 구현한 세션이 자기 코드를 룰과 대조하면, 룰을 읽고 "지켰다" 고 판단한 그 맹점으로 다시 봐요. +입력은 아래 §입력 뿐이에요. 구현 대화를 받았으면 대조하지 말고 그 사실만 보고해요: +`❗ 구현 세션과 같은 컨텍스트예요 — 별도 세션에서 호출해야 의미가 있어요.` + +**2. 위반 0건이 정상 결과예요.** 룰 문장이나 이웃 파일에서 근거를 짚을 수 없는 지적은 올리지 않아요. 취향은 발견이 아니에요. + +## 입력 + +- `rules-audit-scope.sh --spec --json` 의 결과 — `files[]`(변경 파일 · 새 파일 여부 · 걸린 룰 파일) · + `always[]`(Constitution · 경로 없는 spirit 룰) · `new_files[]` · `review_file` +- diff 범위 — `git diff ...HEAD` 와 워킹 트리 (`base` 는 위 결과에 있어요) +- 산출물 경로 — `review_file` (`.ax/docs/spec//review-rules.md`) + +## 하는 것 + +1. **룰 대조** — `rule_files` 를 전부 Read 해요. 파일마다 걸린 룰(`files[].rules`) + 상시 룰(`always`)의 각 `## SP-…` · + `AX:…` 항목을 그 파일의 diff 와 대조해요. 위반이면 `<파일>:<줄>` · 룰 토큰 · 룰 문장 한 줄을 적어요. +2. **이웃 관례 대조** — `new_files` 와, 기존 파일에 새로 생긴 클래스·함수·타입마다: + - **위치** — 같은 레이어의 이웃 디렉토리를 `ls`·`grep` 해요. 이웃이 `projection/` · `vo/` · `dto/` 같은 하위 디렉토리에 + 같은 종류를 모아 두는데 새 것만 다른 곳(예: 쓰는 파일 안에 중첩 선언)에 있으면 위반이에요 + - **이름 꼴** — 이웃의 접두·접미(`*Command` · `*Response` · `*Impl` …)와 다르면 위반이에요 + - **분리** — 이웃은 한 파일에 한 타입인데 새 것만 여러 타입을 한 파일에 넣었으면 위반이에요 + 근거는 늘 이웃 파일 경로 2개 이상이에요. 이웃이 한 개뿐이거나 서로 다르면 관례가 아니라 비차단에 적어요. + +## 하지 않는 것 + +- spec·ADR 적합성 · 빠진 엣지 · 재현 시나리오 — `evaluator` 의 범위예요 (같은 지적이 두 번 와요) +- 구조 대안 · 트레이드오프 — `architect` 의 범위예요 +- 코드 수정 · 빌드 · 테스트 실행 — 읽고 대조만 해요. 고치는 건 코디네이터가 task 로 옮겨서 해요 +- `rule_files` 밖의 룰을 찾아 적용하기 — 범위는 스크립트가 정했어요. 빠진 룰이 보이면 비차단에 "범위 밖" 으로 적어요 + +## 출력 — `review-rules.md`, 첫 줄은 verdict + +`tasks-gate.sh` G7 이 **첫 줄**을 읽어요. 기존 파일은 덮어써요. + +```markdown +verdict: 진행 | 보강 필요 + +## 룰 대조 +- <파일>:<줄> — <룰 토큰> <룰 문장 한 줄> — <무엇이 어긋났나> + +## 이웃 관례 +- <새 파일·심볼> — <관례> (근거: <이웃 경로 1>, <이웃 경로 2>) — <어디로·어떻게> + +## 비차단 +- 반영해도 되고 안 해도 되는 것. verdict 에 세지 않아요. 없으면 "없음" + +## 검토 범위 +- 변경 개 · 룰 파일 개 · 새 파일 개 +``` + +- `진행` — 차단 위반 0건 (`## 비차단` 만 있어도 `진행` 이에요) +- `보강 필요` — 룰 위반 또는 근거 2개 이상의 관례 위반이 1건 이상 + +위반이 없으면 `verdict: 진행` 아래 `위반 0건 — 검토 범위: 변경 N개 · 룰 파일 M개` 한 줄이면 돼요. + +**길이 예산 — 파일 전체 약 1~2천 토큰.** 지적 하나는 포인터 + 룰 토큰 + 한 줄이에요. 코드를 옮겨 붙이지 않아요. + +**돌아올 때는 한 줄이에요** — `verdict · 위반 N건 · 비차단 M건 · 파일 경로`. + +## 안티 패턴 + +- **verdict 없이 끝내기** — 게이트가 못 읽어요. 첫 줄이 `verdict:` 가 아니면 리뷰가 없던 게 돼요 +- **결과를 대화로만 돌려주기** — 코디네이터가 옮겨 적으면 검사받는 쪽이 검사 기록을 쓰는 거예요 +- **근거 없는 관례 지적** — 이웃 경로를 못 대면 발견이 아니에요 +- **룰 파일을 안 읽고 토큰 이름만 보고 판정** — 룰 문장이 근거예요 diff --git a/changelog/0.7.10.md b/changelog/0.7.10.md new file mode 100644 index 0000000..d24f569 --- /dev/null +++ b/changelog/0.7.10.md @@ -0,0 +1,47 @@ +# 0.7.10 — 다음 task 가 의존을 봐요 · 완료 전에 룰을 대조해요 · 끝난 spec 에 후속을 이어 열어요 · 합의 리뷰 상한에서 사용자 승인 + +## 무엇이 좋아졌나 — 누구에게 체감되나 + +> spec 을 tasks 로 나눠 구현하는 사람에게 체감돼요. 다섯 가지 모두 commerce 레포에서 실제로 겪은 마찰이에요. + +- **다음 task 가 의존을 지켜요.** `mark-task.sh --next` 가 `의존:` 을 무시하고 펜스 밖 첫 미완료 task 를 골라, + 아직 안 끝난 task 에 기대는 일을 먼저 시작했어요. 이제 의존이 전부 `[x]`·`[~]` 인 첫 task 를 골라요 + (`tasks-plan.sh` 와 같은 파서). 미완료가 전부 막혀 있으면 "끝났다" 가 아니라 `warning` 과 막힌 task · + 풀리지 않은 의존(없는 ID 는 따로)을 돌려줘요 — 완료 게이트로 잘못 넘어가지 않아요. +- **새 파일이 관례 위치로 가요.** `files:` 목록이 관례 위치(같은 레이어의 `projection/`·`vo/`·`dto/`)를 빠뜨리면, + 구현자가 목록을 지키려고 새 타입을 목록 안 파일에 끼워 넣었어요. spec-tasks 는 새 파일 경로를 적기 전에 이웃을 + grep 하고, 레인·위임 브리프는 "목록과 관례가 충돌하면 멈추고 보고" 를 실어요. +- **완료 전에 룰을 대조해요 (G7).** 위반된 룰(이름 `**/*.kt`, 도메인 `**/domain/**`)은 편집 때 훅이 실제로 + 매칭해서 안내했어요. 그런데 안내 훅은 룰 **경로**만 주고, `rule-read-gate.sh` 는 그 파일을 **읽었는지**만 봐요 — + 읽은 룰을 지켰는지, 새 파일이 이웃 관례를 따랐는지는 아무도 대조하지 않았어요. 이제 새 컨텍스트의 + `rules-auditor` 가 변경 파일마다 걸린 룰과 새 심볼의 이웃 관례를 대조해 `review-rules.md` 첫 줄에 verdict 를 쓰고, + 완료 게이트가 G6 과 같은 필수 여부(size L 이상 · M×L3)로 읽어요. + - 편집 훅도 조금 더 줘요 — 세션에서 처음 매칭될 때 룰 파일 경로와 함께 룰 제목(`SP-…: …`)을 붙여요. +- **끝난 spec 에 후속 작업을 이어 열어요.** `reset-task.sh` 로 닫은 작업은 `--activate` 가 "작업이 없어요" 로 실패해 + 새로 열면 spec 연결이 끊겼어요. `update-task.sh --start --follow-up ` 가 spec_id · spec_dir · spec_tier 를 + 이어받고 `follow_up_of` 를 남겨요. +- **합의 리뷰 상한에서 사용자가 승인할 수 있어요.** 상한에 닿은 뒤 남은 차단이 기계적 수정뿐이어도 진행하려면 + intent_notes 에 손으로 적어야 했고, `--status` 는 그걸 몰라 pass 가 안 됐어요. `spec-review.sh --override --reason` + 이 지금 sha 에 누가 · 언제 · 왜 를 `review-override.md` 로 남기고 `--status` 가 그걸 인정해요. 본문을 또 고치면 풀려요. + +## 바뀐 것 + +- `common.sh` `goax_tasks_parse` — tasks.md 실행 그래프 파서 하나 (`tasks-plan.sh` · `mark-task.sh --next` 공용). + `의존:` 줄은 T-ID 만 읽고, T-ID 가 없는 다른 표기는 풀리지 않는 의존으로 남겨요 (오타가 "의존 없음" 으로 통과하지 않게) +- `tasks-plan.sh` — 의존이 `[~]` 보류여도 풀린 걸로 봐요 (`--next`·G1 과 같은 기준) +- `mark-task.sh --next` — 의존 판정 · 전부 막힘이면 `status: warning` · `result.blocked` · `blocked_tasks[]` +- `rules-audit-scope.sh` (신규) — 변경 파일마다 걸린 spirit·모듈 룰(편집 훅과 같은 매칭 함수) · 상시 룰 · 새 파일 +- `agents/rules-auditor.md` (신규) · `tasks-gate.sh` G7 (`rules_review_required` · `rules_review_verdict`) +- `agents/evaluator.md` — 구현 리뷰의 "하지 않는 것": 이름·패키지 관례 대조는 rules-auditor 범위 +- `spec-implement` — §1 막힘 처리 · §3.3 위임 브리프 · §8.1.5 룰 대조 · 절대 금지 2줄 +- `spec-tasks` · `lane` §10 브리프 · `agents/lane-worker.md` — 관례 위치 / 충돌 시 멈추고 보고 +- `update-task.sh --follow-up` · `triage` 사용법 한 줄 · 없는 작업 에러가 `--follow-up` 을 알려줘요 +- `spec-review.sh --override --reason` (상한 도달 전·사유 없음은 거부, `--dry-run` 무기록) · `spec-validate` 상한 절 +- `pre-edit/spirit-rules-inject.sh` — 경로와 함께 룰 제목 (파일당 15줄, 세션당 처음만) +- `subagent-start/harness-pointer.sh` — rules-auditor 는 자기 에이전트로 제외 + +## 검증 + +- smoke §65 — `--next` 의존·막힘·T-ID 없는 의존 · 관례 위치 문구 4곳 · rules-audit-scope 범위 · G7 없음/보강/진행 · + rules-auditor 배선 · 훅의 룰 제목 · `--follow-up` 상속·거부 · `--override` 상한 전/사유 없음/dry-run/sha 고정 +- smoke §29(G6) fixture 는 G7 진행 파일을 같이 둬요 diff --git a/changelog/README.md b/changelog/README.md index f975abf..3e82a45 100644 --- a/changelog/README.md +++ b/changelog/README.md @@ -4,6 +4,7 @@ ## 버전 +- [0.7.10](0.7.10.md) — `--next` 가 `의존:` 을 봐요 · 완료 전 룰 대조(G7 · `rules-auditor`) · 관례 위치 · 끝난 spec 후속 작업(`--follow-up`) · 합의 리뷰 상한의 사용자 승인(`--override`) - [0.7.9](0.7.9.md) — 레인이 보고·검증 뒤 스스로 정리돼요 (`TaskStop` · `ListAgents` 대조) · 레인은 브리프가 허락한 외부 쓰기만 · 종료 응답은 객체로 - [0.7.8](0.7.8.md) — 화면을 만들고 재고 고친 뒤에만 보여주는 `screen` skill · `screen-designer` (외부 도구 없이도, 게이트 20개를 숫자로) - [0.7.7](0.7.7.md) — 병렬 작업이 서로 상태를 덮지 않아요 (`.ax/tasks/`) · 스킬 description 을 발동 조건만으로 · 훅 출력 10,000자 상한 · 에러가 다음 할 일을 알려줘요 diff --git a/skills/lane/SKILL.md b/skills/lane/SKILL.md index 8f90cb0..268956d 100644 --- a/skills/lane/SKILL.md +++ b/skills/lane/SKILL.md @@ -263,6 +263,10 @@ bash .ax/scripts/bash/tasks-plan.sh --spec "$SPEC" --json `grep` 하고 결과를 보고에 포함하세요. - **공유 이름을 바꾸면 즉시 보고하세요.** props 이름·타입 축·토큰 이름을 바꾸는 건 파일 안의 일이 아니라 레인 밖의 일이에요. +- **소유 목록과 레포 관례가 충돌하면 멈추고 보고하세요.** 새 타입의 이웃 관례가 별도 파일·별도 + 위치(같은 레이어의 `projection/`·`vo/`·`dto/` 등)인데 그 경로가 목록에 없으면, 목록 안 파일에 + 끼워 넣지 말고 `관례 충돌 — <새 타입>, 이웃 관례 <경로 2개>, 필요한 파일 <경로>` 로 보고해요. + 목록은 코디네이터가 늘려요. - **보고는 한 번, 한 경로로.** 도구 목록에 `SendMessage` 가 있는지로 경로가 정해져요. - 없으면(서브에이전트): **결과를 최종 메시지에 전부 담으세요.** 최종 응답이 유일한 산출물 경로예요. 파일을 다 만들고 idle 이 되는 건 완료가 아니에요. @@ -336,7 +340,7 @@ bash .ax/scripts/bash/tasks-gate.sh --spec "$SPEC" --json 레인 모드에선 원장도 봐요 — `dispatched_unreported`(보고 안 받은 디스패치)와 `done_without_report`(보고 없이 켜진 체크박스)가 비어야 해요. 레인이 자기 체크박스를 켜면 여기서 잡혀요. 그리고 size L 이상 · M×L3 이면 새 컨텍스트 evaluator 의 `review.md` -첫 줄이 `verdict: 진행` 이어야 `complete` 예요 (G6). 체크박스를 채운 세션이 검사까지 +첫 줄이 `verdict: 진행` 이어야 `complete` 예요 (G6). 룰 대조 `review-rules.md`(rules-auditor)도 같아요 (G7). 체크박스를 채운 세션이 검사까지 하면 게이트가 아니라 자기보고라서요. ### 12.2 통합 검증 — 파일이 안 겹쳤다고 합쳐서 도는 건 아니에요 diff --git a/skills/spec-implement/SKILL.md b/skills/spec-implement/SKILL.md index 8eab60f..81ca034 100644 --- a/skills/spec-implement/SKILL.md +++ b/skills/spec-implement/SKILL.md @@ -28,8 +28,14 @@ done # 다음 미완료 task 찾기 — 없으면 끝난 게 아니라 §8 완료 게이트로 가요 (게이트가 완료를 판정해요) # 맨 grep 은 템플릿 펜스 안의 예시(`- [ ] T001 [P] [AC2] <한 줄 설명>`)를 집어요 — mark-task.sh --next 는 펜스 밖만 봐요. -NEXT_TASK=$(bash .ax/scripts/bash/mark-task.sh --spec "$SPEC" --next --json | jq -r '.result.task // empty') -[ -z "$NEXT_TASK" ] && echo "미완료 task 없음 → §8 완료 게이트로" +# 의존(`의존:` 줄)이 전부 [x]·[~] 인 첫 미완료 task 를 골라요 — tasks-plan.sh 와 같은 파서예요. +NEXT_JSON=$(bash .ax/scripts/bash/mark-task.sh --spec "$SPEC" --next --json) +NEXT_TASK=$(echo "$NEXT_JSON" | jq -r '.result.task // empty') +if [ "$(echo "$NEXT_JSON" | jq -r '.result.blocked // false')" = true ]; then + echo "$NEXT_JSON" | jq -r '.next_step' # 미완료는 있는데 전부 의존에 막혔어요 — 끝난 게 아니에요. §6 처럼 halt +elif [ -z "$NEXT_TASK" ]; then + echo "미완료 task 없음 → §8 완료 게이트로" +fi ``` context로 로드: @@ -184,6 +190,19 @@ RECURRENCE=$(grep -lE "^category:.*\\b${TASK_DOMAIN}\\b" .ax/mistakes/*.md 2>/de `[n]` 시 halt + 사용자 결정 대기. +### 3.3 단일 레인에서 task 를 위임할 때 — 브리프에 넣을 것 + +이 세션이 task 하나를 서브에이전트(executor 등)에게 맡기면, 브리프에 그 task 의 `files:` 목록과 함께 이 줄을 넣어요: + +``` +files: 목록과 레포 관례(새 파일 위치·분리)가 충돌하면 — 예: 새 타입의 이웃 관례는 같은 레이어 projection/·vo/·dto/ +같은 별도 파일인데 그 경로가 목록에 없으면 — 목록 안 파일에 끼워 넣지 말고 멈추고 보고하세요 +(`관례 충돌 — <새 타입>, 이웃 관례 <경로 2개>, 필요한 파일 <경로>`). +``` + +보고가 오면 tasks.md 의 `files:` 를 관례 경로로 고치고 다시 맡겨요. 목록을 지키려고 관례를 어긴 코드는 G7(§8.1.5)에서 +다시 잡혀 되돌아와요. + ### 3.5 레인 모드 — 코디네이터 루프 `LANE_N > 0` 이면 이 세션은 **코디네이터**예요 — 코드를 직접 고치지 않고 `lanes-dispatch.sh --dispatch` 로 @@ -315,12 +334,15 @@ fi | `REVIEW_REQ` | `VERDICT` | 할 것 | |---|---|---| -| true | 없음 | **§8.1 evaluator 호출** — 필수예요 (size L 이상 · M×L3) | +| true | 없음 | **§8.1 evaluator 호출** — 필수예요 (size L 이상 · M×L3). §8.1.5 룰 대조와 한 메시지에 같이 띄워요 | | false | 없음 | `EFFECTIVE_MODE = autopilot` 이면 생략, 그 외엔 한 번만 제안: "evaluator 리뷰 돌릴까요?" | | * | `진행` | §8.2 완료 | | * | `보강 필요` | review.md 의 지적 하나하나를 task 로 옮겨요 (`- [ ] T1NN [ACn] <지적> — files: …`), §3 으로 돌아가요. 끝나면 evaluator 를 **다시** 띄워요 — review.md 는 evaluator 가 덮어써요 | | * | `재논의 필요` | halt. 사용자 결정 — spec 자체를 다시 봐야 한다는 뜻이에요 | +룰 대조(G7 — `review-rules.md`)도 같은 표예요: 필수 여부는 `REVIEW_REQ` 와 같고, `RULES_VERDICT=$(echo "$GATE" | jq -r +'.result.rules_review_verdict // ""')` 가 없으면 §8.1.5, `보강 필요` 면 위반을 task 로 옮겨 §3 으로 갔다가 rules-auditor 를 다시 띄워요. + `재논의 필요`·`보강 필요` 로 이 턴이 그냥 끝나면 `stop/spec-gate.sh` 가 한 번 더 잡아요 — phase 가 `implementing`/`review` 인데 `tasks-gate.sh` 가 아직 실패면, §6 의 인계 노트(24시간 이내)가 없는 한 턴 종료를 한 번 막고 되돌려보내요. 정상 완료(§8.2, `reset-task.sh` 가 phase 를 `idle` 로 @@ -348,12 +370,29 @@ evaluator 가 파일을 직접 써요. 코디네이터는 결과를 받아 적 쪽이 검사 기록을 쓰게 돼요. 돌아오면 `tasks-gate.sh` 를 다시 돌려요. G6 이 review.md 의 첫 줄을 읽어 판정해요. `보강 필요` 로 §3 에 돌아갈 땐 phase 를 다시 `implementing` 으로 되돌려요. +### 8.1.5 룰 대조 — rules-auditor, 새 컨텍스트로 (G7) + +편집 훅은 룰 경로를 알려주고 `rule-read-gate.sh` 는 읽었는지만 봐요 — 지켰는지, 새 파일이 이웃 관례(위치·이름·분리)를 +따랐는지는 evaluator 의 범위도 아니에요. 그래서 완료 전에 따로 대조해요. 범위는 스크립트가 정해요: + +```bash +SCOPE=$(bash .ax/scripts/bash/rules-audit-scope.sh --spec "$SPEC" --json) # 변경 파일마다 걸린 룰 · 상시 룰 · 새 파일 +echo "$SCOPE" | jq -r '.next_step' +``` + +`Agent` 도구로 `goax:rules-auditor` 를 띄워요 (vendor 설치면 `rules-auditor`) — evaluator 와 **한 메시지에 같이**, 서로의 +파일은 브리프에 넣지 않아요. 브리프는 `SCOPE` 의 result(JSON 그대로) · diff 범위(§8.1 과 같은 base) · 산출물 경로 +(`result.review_file`, 첫 줄 `verdict: 진행 | 보강 필요`) 뿐이에요. 이 대화는 넘기지 않아요. 파일은 rules-auditor 가 직접 +써요 — 코디네이터가 받아 적지 않아요. 돌아오면 `tasks-gate.sh` 를 다시 돌려요. `SCOPE` 가 skipped(변경 0)면 브리프 대신 +`review-rules.md` 를 rules-auditor 에게 "변경 0 — 진행" 으로 쓰게 해요. + ### 8.2 완료 ```bash if [ "$COMPLETE" = "true" ]; then echo "✅ spec $SPEC 구현 완료." [ -f "$SPEC_DIR/review.md" ] && echo " ✅ evaluator verdict: $(head -1 "$SPEC_DIR/review.md")" + [ -f "$SPEC_DIR/review-rules.md" ] && echo " ✅ 룰 대조 verdict: $(head -1 "$SPEC_DIR/review-rules.md")" 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 @@ -410,6 +449,8 @@ fi - **레인 보고를 받고 검증 명령 없이 마킹** — "테스트 통과했어요" 는 주장이고, 코디네이터가 돌린 명령의 exit 0 이 근거예요 - **코디네이터가 review.md 를 쓰기** — evaluator 의 출력을 받아 적는 순간 검사받는 쪽이 검사 기록을 쓰는 거예요 - **evaluator 에 이 대화를 넘기기** — 새 컨텍스트가 성립 조건이에요 (`agents/evaluator.md` §성립 조건) +- **코디네이터가 review-rules.md 를 쓰기 · 룰 대조를 evaluator 에 얹기** — G7 은 rules-auditor 가 새 컨텍스트로 써요. evaluator 는 이름·패키지 관례를 보지 않아요 +- **files: 목록을 지키려고 새 타입을 목록 안 파일에 끼워 넣기** — 관례와 충돌하면 멈추고 tasks.md 를 고쳐요 (§3.3) - **`review_required = true` 인데 evaluator 생략** — G6 이 막지만, 막히기 전에 안 하는 게 맞아요 - 실패 / 범위이탈 / elevated 시 자동 우회 - 완료 task 를 다시 - [ ] 로 되돌리기 diff --git a/skills/spec-tasks/SKILL.md b/skills/spec-tasks/SKILL.md index 58fa1ed..86941f6 100644 --- a/skills/spec-tasks/SKILL.md +++ b/skills/spec-tasks/SKILL.md @@ -91,7 +91,7 @@ task 가 있으면 그건 spec 에 없는 일을 하고 있다는 신호예요. | `[P]` | **병렬 가능** — 미완료 의존 없음 + 다른 `[P]` task 와 파일 안 겹침, 둘 다 만족할 때만 | | `[AC2]` | 이 task 가 충족하는 spec.md §3 의 수용 기준 | | `files:` | 건드리는 파일. **모든 task 에 필수** — 빠지면 겹침 검사가 그 task 엔 안 닿아서 "위반 없음"이 "검사 안 함"이 돼요 | -| `의존:` | 앞선 task ID. 없으면 `없음`·`none`·`-` 중 하나만 인정돼요 (`deps:`·`depends:` 는 안 읽혀요) | +| `의존:` | 앞선 task ID. 없으면 `없음`·`none`·`-` 중 하나만 인정돼요 (`deps:`·`depends:` 는 안 읽혀요). `tasks-plan.sh` 와 `mark-task.sh --next` 가 이 줄로 다음 task 를 골라요 — 의존이 `[x]`·`[~]` 가 돼야 풀려요 | 체크박스는 셋 — `[ ]` 미완료 · `[x]` 완료 · `[~]` 의도적 보류(사유 병기). @@ -130,6 +130,10 @@ task 가 있으면 그건 spec 에 없는 일을 하고 있다는 신호예요. 규칙: - **모든 task 에 `files:` 필수** — 겹침 검사가 이 값으로만 성립해요 (`lanes-hotfiles.sh` 의 `tasks_missing_files` 로 사각지대 확인) +- **새 파일을 만드는 task 는 `files:` 를 적기 전에 같은 레이어 이웃을 grep 해요** — 이웃이 같은 종류를 + `projection/`·`vo/`·`dto/` 같은 하위 디렉토리에 따로 두면 새 파일 경로도 거기로 정해요. `files:` 는 구현자에겐 + "이 밖은 손대지 마" 라서, 관례 위치가 빠진 목록을 받은 구현자는 새 타입을 목록 안 파일에 끼워 넣어요 (commerce 실측). + 예: `ls $(dirname <비슷한 기존 파일>)/` · `grep -rl "interface .*Projection" <모듈>/` 으로 이웃 2개 이상을 확인 - Phase 순서: setup → foundational → user stories(priority순) → polish - TaskID는 sortable (T001, T010, T100) - `[P]` 는 기본값이 아니라 증명될 때만 — 판단이 서지 않으면 붙이지 않아요 diff --git a/skills/spec-validate/SKILL.md b/skills/spec-validate/SKILL.md index 1c0663e..eedb88d 100644 --- a/skills/spec-validate/SKILL.md +++ b/skills/spec-validate/SKILL.md @@ -125,6 +125,16 @@ printf '%s\n' "$REVIEW" | jq -r '"architect \(.result.architect.verdict // "없 사용자 결정으로 halt. **라운드가 상한을 넘으면 지적을 더 반영하려 하지 말고 그 자체로 halt** — 라운드를 더 돌리는 건 리뷰가 아니라 spec 정의가 문제라는 신호예요. `--snapshot` 응답의 `warnings[]` 가 상한 경고를 줘요. + 상한에 닿은 뒤 남은 차단이 기계적 수정(오타·링크·이름 맞춤)뿐이라 사용자가 **이대로 진행**을 정하면, 고친 뒤 + 그 결정을 지금 sha 에 기록해요 — intent_notes 에 손으로 적지 않아요 (`--status` 가 못 읽어요): + +```bash +bash .ax/scripts/bash/spec-review.sh --spec "$SPEC" --override --reason "<사용자 결정 한 줄 — 무엇을 왜 받아들였나>" --json +``` + + `review-override.md`(커밋해요)에 sha · 누가(git user.name) · 언제 · 두 verdict · 사유가 남고, `--status` 는 그 sha 일 + 때만 pass 로 봐요. 상한 전(라운드 < 상한)에는 거부돼요. 사용자에게 묻지 않고 이 세션이 정해서 쓰지 않아요 — + `--reason` 은 사용자가 한 말이에요. 기록 뒤 본문을 또 고치면 승인이 풀려요. 5. `pass=true` 면 합본을 남기고 §3 으로: ```bash diff --git a/skills/triage/SKILL.md b/skills/triage/SKILL.md index b759b98..3ee3307 100644 --- a/skills/triage/SKILL.md +++ b/skills/triage/SKILL.md @@ -226,6 +226,8 @@ bash .ax/scripts/bash/update-task.sh --start --phase triaged \ 작업마다 `.ax/tasks/.json` 에 남고, current-task.json 은 지금 작업의 사본이에요. 앞 작업으로 돌아갈 땐 `update-task.sh --task --activate`, 다른 세션의 작업을 건드리지 않고 내 작업만 고칠 땐 `--task ` 를 붙여요. 같은 작업을 이어 하는 거면 `--start` 하지 않아요 (같은 id 로 `--start` 하면 거부돼요). +**이미 끝난 spec 의 후속 작업**이면 (`reset-task.sh` 로 닫혀 `--activate` 가 "작업이 없어요" 로 실패해요) 새 id 로 +`update-task.sh --start --follow-up --set task_id=$TASK_ID …` — spec_id · spec_dir · spec_tier 를 그 spec 에서 이어받아요. **작업 id 를 사용자에게 한 번 보여주고 이 대화 내내 기억해요** — spec · spec-tasks · spec-validate · spec-implement 가 `--task ` 로 넘겨요. 병렬 작업이 있는데 `--task` 가 없으면 update-task 가 덮지 않고 되물어요. diff --git a/skills/vendor/SKILL.md b/skills/vendor/SKILL.md index d4f5b55..bed632c 100644 --- a/skills/vendor/SKILL.md +++ b/skills/vendor/SKILL.md @@ -74,10 +74,10 @@ bash .ax/scripts/bash/vendor-skills.sh --plugin-dir "$PLUGIN_ROOT" --dry-run --j 프로젝트 루트 commerce-monorepo/projects/commerce/ (.ax/ 가 여기) → 둘이 달라요. .goax-root 포인터를 만들게요. - 📂 동봉 대상 — 20개 + 📂 동봉 대상 — 22개 .claude/skills/ 15개 (up, onboarding, zero, triage, spec, lane, adr, …) .claude/commands/ 1개 (goax) - .claude/agents/ 4개 (architect, evaluator, lane-scout, lane-worker) + .claude/agents/ 6개 (architect, evaluator, lane-scout, lane-worker, rules-auditor, screen-designer) ❗ 기존 파일 같은 이름의 goax 자산만 덮어써요. 직접 만드신 스킬은 건드리지 않아요. diff --git a/templates/default/.ax/_templates/spec/tasks.md b/templates/default/.ax/_templates/spec/tasks.md index d7ed3d4..55304a7 100644 --- a/templates/default/.ax/_templates/spec/tasks.md +++ b/templates/default/.ax/_templates/spec/tasks.md @@ -59,9 +59,10 @@ task 는 다른 task 와 같은 파일을 써도 violation 이 안 나와요 — 이라고 말하는데 검사가 안 된 거예요. **없는 것과 확인 안 한 것은 달라요.** 사각지대 목록은 `lanes-hotfiles.sh` 의 `tasks_missing_files` 로 확인해요. -`의존:` 도 표기가 고정이에요 (`tasks-plan.sh`). `deps:`·`depends:` 는 안 읽히고, -"의존 없음"으로 인정되는 값은 `없음`·`none`·`-` 뿐이에요. 다른 표기는 그 이름의 -task 에 의존하는 걸로 읽혀서 영원히 blocked 가 돼요. +`의존:` 도 표기가 고정이에요 (`tasks-plan.sh` · `mark-task.sh --next` 가 같은 파서로 읽어요). +`deps:`·`depends:` 는 안 읽히고, 줄에서 읽는 건 T-ID 예요 (`T001, T041 (스키마)` → T001·T041). +"의존 없음"으로 인정되는 값은 `없음`·`none`·`-` 뿐이에요. T-ID 가 없는 다른 표기는 없는 task 에 +의존하는 걸로 읽혀서 영원히 blocked 가 돼요. 의존 task 가 `[x]` 이거나 `[~]` 면 풀린 거예요. ### Phase 1: <이름> @@ -99,11 +100,11 @@ bash .ax/scripts/bash/tasks-gate.sh --spec --json ``` `[ ]` 미완료 수, AC 커버리지 간극, 매핑 없는 orphan task, 원장 불일치(보고 안 받은 디스패치 · -보고 없이 켜진 체크박스), evaluator verdict 를 한 번에 알려줘요. +보고 없이 켜진 체크박스), evaluator verdict, 룰 대조 verdict 를 한 번에 알려줘요. 완료는 `complete: true` 하나예요. size L 이상 · M×L3 이면 새 컨텍스트 evaluator 가 -`review.md` 첫 줄에 `verdict: 진행` 을 써야 거기 도달해요 — 체크박스를 채운 세션이 -검사까지 하면 게이트가 아니에요. +`review.md` 첫 줄에, 새 컨텍스트 rules-auditor 가 `review-rules.md` 첫 줄에 `verdict: 진행` 을 써야 +거기 도달해요 — 체크박스를 채운 세션이 검사까지 하면 게이트가 아니에요. > 워크플로우 안내 (다음 단계) 는 `/spec-implement` skill 출력의 `📍 다음` 으로 제공돼요. > 환경 검증·배포는 이 문서 범위 밖 — 별도 운영 채널. 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 8a168b6..5f3c827 100755 --- a/templates/default/.ax/hooks/pre-edit/spirit-rules-inject.sh +++ b/templates/default/.ax/hooks/pre-edit/spirit-rules-inject.sh @@ -146,10 +146,19 @@ for m in "${MATCHED[@]}"; do done [ ${#FRESH[@]} -eq 0 ] && exit 0 -# additionalContext 생성 (룰 파일 경로 알림 — Claude가 필요 시 Read) +# additionalContext 생성 — 룰 파일 경로 + 그 파일의 룰 제목(`## SP-…: 제목`, 펜스 밖, 파일당 15줄) +# 경로만 주면 모델이 "무슨 룰인지" 를 Read 해야만 알아요 — rule-read-gate.sh 가 Read 는 강제하지만, 제목이 함께 +# 보이면 편집하는 그 줄을 어느 룰에 비춰 볼지 바로 떠올라요. 세션당 처음 매칭될 때만(goax_inject_fresh) 붙으니 +# 비용은 룰 파일 하나에 제목 몇 줄이에요. 본문은 넣지 않아요 (B-pointer — 본문은 Read 로). +rule_titles() { + awk '/^[[:space:]]*```/{f=!f; next} f{next} + /^## SP-[[:alnum:]]+-[0-9]+:/ { sub(/^## /, ""); if (++n <= 15) print " " $0; else if (n == 16) print " …" }' "$1" 2>/dev/null || true +} LIST="" for m in "${FRESH[@]}"; do LIST+="- $m"$'\n' + T=$(rule_titles "$PROJECT_ROOT/$m") + [ -n "$T" ] && LIST+="$T"$'\n' done CTX="📋 Path-scoped spirit rules apply to ${TARGET_REL} — Read these before editing if not yet:"$'\n'"$LIST" # 룰 파일이 많아도 10,000자 상한(넘으면 파일로 빠져요) 아래로 diff --git a/templates/default/.ax/hooks/stop/spec-gate.sh b/templates/default/.ax/hooks/stop/spec-gate.sh index bba3bf0..b814cbe 100755 --- a/templates/default/.ax/hooks/stop/spec-gate.sh +++ b/templates/default/.ax/hooks/stop/spec-gate.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Stop hook — 활성 spec 이 미완료인 채 턴이 끝나려 하면 **한 번** 멈춰 세워요 # -# 왜 필요한가: 완료 게이트(G1~G6)는 파일 게이트라 커밋 시점(spec-completion-gate.sh)엔 걸리지만, +# 왜 필요한가: 완료 게이트(G1~G7)는 파일 게이트라 커밋 시점(spec-completion-gate.sh)엔 걸리지만, # 커밋 없이 턴이 끝나면 아무도 안 봐요. 실사용 리포에서 spec 21개 중 14개가 미완료 task 를 남긴 채 # 끝나 있었고, 그 세션들은 커밋도 인계 노트도 없이 그냥 멈췄어요. 턴 종료는 스킬을 우회해도 # 반드시 지나는 지점이에요. diff --git a/templates/default/.ax/hooks/subagent-start/harness-pointer.sh b/templates/default/.ax/hooks/subagent-start/harness-pointer.sh index 134f8e3..a85b6f7 100755 --- a/templates/default/.ax/hooks/subagent-start/harness-pointer.sh +++ b/templates/default/.ax/hooks/subagent-start/harness-pointer.sh @@ -28,7 +28,7 @@ command -v jq >/dev/null 2>&1 || exit 0 INPUT="$(cat 2>/dev/null || true)" AGENT=$(printf '%s' "$INPUT" | jq -r '.agent_type // ""' 2>/dev/null || true) case "$AGENT" in - goax:*|evaluator|architect|lane-scout|lane-worker) exit 0 ;; # 자기 에이전트 — 이미 spirit 를 선언해요 + goax:*|evaluator|architect|lane-scout|lane-worker|rules-auditor) exit 0 ;; # 자기 에이전트 — 이미 spirit 를 선언해요 esac PROJECT_ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}" diff --git a/templates/default/.ax/scripts/bash/README.md b/templates/default/.ax/scripts/bash/README.md index 7d945df..e5daf3b 100644 --- a/templates/default/.ax/scripts/bash/README.md +++ b/templates/default/.ax/scripts/bash/README.md @@ -7,11 +7,11 @@ | 스크립트 | 용도 | 호출하는 skill | |---|---|---| -| `common.sh` | 공통 함수 (find_project_root, json_output, [goax] log, `goax_inject_fresh` 세션 내 중복 주입 제거, `goax_lock`/`goax_unlock`/`goax_unlock_all` 원장 락, `goax_mktemp` 폴백 임시 파일, `goax_git_hook_path` git 없이도 도는 훅 경로, `goax_resolve_spec` `--spec` 축약 해석, `goax_secret_rules`/`goax_secret_patterns`/`goax_secret_scan_file`/`redact_secrets` 시크릿 패턴 SSOT — 검출과 마스킹이 같은 표에서 나와요 (검출은 kv-detect 전에 TS·Kotlin·Swift 타입 자리를 지워요), `goax_hook_enabled`/`goax_hook_profile` 훅 끄기·프로필, `goax_session_mark`/`goax_session_marked`/`goax_session_count` 세션 마커, `goax_shell_scan` 따옴표·heredoc 을 셸처럼 읽는 명령 판정(bypass·destructive), `goax_module_rules_matching`·`goax_imported_paths` 주입 훅과 게이트가 공유하는 룰 매칭, `goax_doc_id`/`goax_doc_key`/`goax_doc_sort` spec·ADR ID) | (sourced by all) | +| `common.sh` | 공통 함수 (find_project_root, json_output, [goax] log, `goax_inject_fresh` 세션 내 중복 주입 제거, `goax_lock`/`goax_unlock`/`goax_unlock_all` 원장 락, `goax_mktemp` 폴백 임시 파일, `goax_git_hook_path` git 없이도 도는 훅 경로, `goax_resolve_spec` `--spec` 축약 해석, `goax_tasks_parse` tasks.md 실행 그래프(`의존:` 포함 — tasks-plan·mark-task --next 공용), `goax_secret_rules`/`goax_secret_patterns`/`goax_secret_scan_file`/`redact_secrets` 시크릿 패턴 SSOT — 검출과 마스킹이 같은 표에서 나와요 (검출은 kv-detect 전에 TS·Kotlin·Swift 타입 자리를 지워요), `goax_hook_enabled`/`goax_hook_profile` 훅 끄기·프로필, `goax_session_mark`/`goax_session_marked`/`goax_session_count` 세션 마커, `goax_shell_scan` 따옴표·heredoc 을 셸처럼 읽는 명령 판정(bypass·destructive), `goax_module_rules_matching`·`goax_imported_paths` 주입 훅과 게이트가 공유하는 룰 매칭, `goax_doc_id`/`goax_doc_key`/`goax_doc_sort` spec·ADR ID) | (sourced by all) | | `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** 갱신 — 작업별 원본은 `.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` | +| `update-task.sh` | `current-task.json` 의 task 필드를 **락 안에서 in-place** 갱신 — 작업별 원본은 `.ax/tasks/.json` (`--task ` 면 그 작업만 · 지금 작업이 아니면 current-task.json 은 그대로 · `--activate` 로 지금 작업 전환 · `--start --follow-up ` 로 끝난 spec 의 후속 작업을 spec_* 이어받아 열기) — `--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` | @@ -35,11 +35,12 @@ | `doctor-scan.sh` | doctor 의 인라인 진단 셋 — 마이그레이션 잔재(`.ax/docs/STATUS.md` 잔재 통지 포함) · template 기준 hook 등록(파일 + **이벤트 키**) · 문서↔실제 메커니즘 · **도달 지도**(룰 소스별 배관 생사) · **인계 노트 기한**(`current-task.json` `handoff` `- [ ] YYYY-MM-DD`, I3 규칙) | `doctor` | | `constitution-apply.sh` | onboarding Q5 의 Constitution 블록 적용 — `--block` prepend(기존 본문 `---` 아래 보존) · `--scan-duplicates` · `--drop-exact`(사용자 [a] 뒤에만) · `--append-index` | `onboarding` | | `tasks-plan.sh` | tasks.md → ready / blocked / parallel + `[P]` 파일 겹침 violations. 항목별 승격 — wave(배리어) 없음, 자동 실행 없음 | `lane`, `spec-implement` | -| `tasks-gate.sh` | spec 완료 게이트 G1~G6 — 미완료 · AC 커버리지 · orphan · 유실 · 레인 원장 · evaluator verdict(`review.md`) | `spec-implement`, `lane`, pre-commit hook | +| `tasks-gate.sh` | spec 완료 게이트 G1~G7 — 미완료 · AC 커버리지 · orphan · 유실 · 레인 원장 · evaluator verdict(`review.md`) · 룰 대조 verdict(`review-rules.md`) | `spec-implement`, `lane`, pre-commit hook | | `lanes-hotfiles.sh` | tasks.md 에서 핫 파일(여러 미완료 task 가 쓰는 파일) + `files:` 누락 task 추출 | `lane` | -| `spec-review.sh` | spec 합의 리뷰 원장 — `--snapshot`(sha 고정·라운드) / `--status`(리뷰어별 `review-spec.{architect,evaluator}.md` 의 verdict·sha 집계 → pass) / `--merge`(합본). 필수 여부는 Size 축만 | `spec-validate` | +| `spec-review.sh` | spec 합의 리뷰 원장 — `--snapshot`(sha 고정·라운드) / `--status`(리뷰어별 `review-spec.{architect,evaluator}.md` 의 verdict·sha 집계 → pass) / `--merge`(합본) / `--override --reason`(상한 도달 뒤 사용자 승인을 지금 sha 에 기록 — `review-override.md`). 필수 여부는 Size 축만 | `spec-validate` | +| `rules-audit-scope.sh` | 룰 대조(G7) 범위 — 변경 파일마다 걸린 spirit·모듈 룰(편집 훅과 같은 매칭 함수) · 상시 룰(Constitution · `paths:` 없는 spirit 룰) · 새 파일. rules-auditor 브리프의 입력 | `spec-implement` | | `lanes-dispatch.sh` | 레인 디스패치 원장 — `--assign / --dispatch <레인 \| T010,T011> / --report <레인 \| T010,T011> / --status`. tasks.md 의 `레인:`·`디스패치:`·`보고:` 필드를 쓰고, 파일 소유 충돌이면 dispatch 거부 | `lane`, `spec-implement` | -| `mark-task.sh` | tasks.md 체크박스를 켜요 — `--task T013[,T014] [--state x\|~]` · `--next`(펜스 밖 첫 미완료). 줄 **앞** ID 만 매칭(본문 언급 무시), 코드 펜스 건너뜀, `tasks.md.lock`, 쓰기 전후 `[x]` 개수 검증. 여러 ID 는 트랜잭션(하나라도 없으면 무변경) | `spec-implement` | +| `mark-task.sh` | tasks.md 체크박스를 켜요 — `--task T013[,T014] [--state x\|~]` · `--next`(펜스 밖에서 `의존:` 이 전부 `[x]`·`[~]` 인 첫 미완료 — 전부 막혔으면 warning + `blocked_tasks`). 줄 **앞** ID 만 매칭(본문 언급 무시), 코드 펜스 건너뜀, `tasks.md.lock`, 쓰기 전후 `[x]` 개수 검증. 여러 ID 는 트랜잭션(하나라도 없으면 무변경) | `spec-implement` | | `zero-init.sh` | 0→1 첫날 팩 설치 (룰은 라이브 `spirit/rules/`, 템플릿은 `_templates/zero/`) — 덮어쓰지 않고 SP 토큰 충돌만 경고 | `zero` | | `zero-domain-risk.sh` | `config.yml` 의 `domain_risk` 블록 통째 교체 (`--show/--set/--default`) — 출고 예시 키가 남으면 triage 가 영원히 default_risk 로 흘러요 | `zero` | | `zero-probe.sh` | 네거티브 프로브 — 일부러 위반을 만들어 차단이 실제로 도는지 확인 | `zero` | @@ -94,7 +95,7 @@ 달리 **exit code 는 `0`** 이에요 — caller 가 exit 만 보고 넘어가면 이 신호를 놓쳐요, `status` 를 같이 봐야 해요. `warning` 을 emit 하는 스크립트: `check-manifest-install.sh`, `check-rule-enforcement.sh`, `check-sensor-liveness.sh`, `constitution-apply.sh`, `doctor-scan.sh`, `lanes-dispatch.sh`, -`lanes-hotfiles.sh`, `rules-index.sh`, `spec-review.sh`, `spirit-lint.sh`, `status-note.sh`, +`lanes-hotfiles.sh`, `mark-task.sh`(`--next` 전부 의존에 막힘), `rules-index.sh`, `spec-review.sh`, `spirit-lint.sh`, `status-note.sh`, `tasks-gate.sh`, `tasks-plan.sh`, `zero-ablation.sh`. 예외: `update-state.sh --json` 은 이 envelope 을 따르지 않아요 — 호출자가 `.status`/`.result` 를 diff --git a/templates/default/.ax/scripts/bash/common.sh b/templates/default/.ax/scripts/bash/common.sh index c840f11..825b167 100755 --- a/templates/default/.ax/scripts/bash/common.sh +++ b/templates/default/.ax/scripts/bash/common.sh @@ -1510,6 +1510,69 @@ goax_resolve_spec() { return 1 } +# goax_tasks_parse +# tasks.md 를 실행 그래프 한 줄씩으로 읽어요 — tasks-plan.sh · mark-task.sh --next 가 같은 파서를 써요 +# (둘이 다르게 읽으면 plan 은 blocked 라는데 --next 는 그 task 를 집어요). +# 출력: "ID|state|P|files(,)|deps(,)|line" state = open|done|paused · P = 0|1 · line = task 줄 번호 +# - ``` 코드펜스 안은 건너뛰어요 (템플릿의 형식 설명 예시가 실 task 로 세지 않게) +# - files: 경로는 정규화해요 (`./` 제거 · 중복 `/` · 끝 `/`) — 표기 차이로 겹침 검사를 빠져나갈 수 없어요 +# - deps 는 task 다음 들여쓰기 `의존:` 줄의 T-ID 만 뽑아요 — `없음`·`none`·`-` 은 빈 값, +# `T001 (스키마)` 처럼 설명이 붙어도 T001 만 남아요. T-ID 가 하나도 없는 다른 표기는 원문 그대로 남겨 +# (없는 task 에 의존한 것처럼) 풀리지 않게 해요 — 오타가 "의존 없음" 으로 조용히 통과하지 않게요 +goax_tasks_parse() { + awk ' + function norm(p) { + gsub(/\/+/, "/", p) + while (sub(/^\.\//, "", p)) ; + while (sub(/\/\.\//, "/", p)) ; + sub(/\/+$/, "", p) + return p + } + function normlist(s, n, a, i, v, out) { + if (s == "") return "" + n = split(s, a, ",") + out = "" + for (i = 1; i <= n; i++) { v = norm(a[i]); if (v != "") out = (out == "" ? v : out "," v) } + return out + } + function flush() { + if (id != "") printf "%s|%s|%s|%s|%s|%s\n", id, st, par, files, deps, ln + id=""; st=""; par="0"; files=""; deps=""; ln="" + } + /^[[:space:]]*```/ { fence = !fence; next } + fence { next } + /^- \[[ x~X]\] / { + flush() + line = $0; ln = NR + st = "open" + if (line ~ /^- \[[xX]\]/) st = "done" + else if (line ~ /^- \[~\]/) st = "paused" + if (match(line, /\[P\]/)) par = "1" + if (match(line, /T[0-9][0-9][0-9]+/)) id = substr(line, RSTART, RLENGTH) + f = line + if (match(f, /files:[ ]*/)) { + f = substr(f, RSTART + RLENGTH) + gsub(/[ ]*,[ ]*/, ",", f); gsub(/^[ ]+|[ ]+$/, "", f) + files = normlist(f) + } + next + } + id != "" && /^[ \t]+의존:/ { + d = $0; sub(/^[ \t]*의존:[ ]*/, "", d); raw = d + while (match(d, /T[0-9]+/)) { + v = substr(d, RSTART, RLENGTH); d = substr(d, RSTART + RLENGTH) + deps = (deps == "" ? v : deps "," v) + } + # T-ID 가 없는데 없음/none/- 도 아니면 읽을 수 없는 의존 — 조용히 "의존 없음" 으로 풀지 않고 + # 그대로 남겨요 (tasks.md 에 없는 ID 처럼 영원히 안 풀려서 눈에 띄어요) + gsub(/^[ \t]+|[ \t]+$/, "", raw) + if (deps == "" && raw !~ /^(없음|none|-)?$/) { gsub(/[|,]/, ";", raw); deps = raw } + next + } + END { flush() } + ' "$1" +} + # Find project root — fallback chain: # 1) $GOAX_PROJECT_DIR — CLI-agnostic override. Claude Code 외 환경 (직접 호출, CI, # 다른 AI CLI 의 어댑터) 에서 결정론 스크립트를 standalone 으로 부를 때 사용. diff --git a/templates/default/.ax/scripts/bash/mark-task.sh b/templates/default/.ax/scripts/bash/mark-task.sh index e3aba12..7a3001f 100644 --- a/templates/default/.ax/scripts/bash/mark-task.sh +++ b/templates/default/.ax/scripts/bash/mark-task.sh @@ -11,14 +11,20 @@ # 쓰기 전후 [x] 개수가 바꾼 줄 수만큼만 늘었는지(이미 켜져 있으면 0) 확인. # --task T013,T014 — 쉼표로 여러 개. 트랜잭션이에요: 하나라도 없거나 중복이면 아무것도 안 켜요. # 이미 켜진 것은 건너뛰고 unchanged_tasks 에 적어요. -# --next 는 읽기만 해요 — 펜스 밖 첫 미완료 task (템플릿 펜스 안의 예시 줄을 집지 않게). +# --next 는 읽기만 해요 — 펜스 밖에서 **의존(`의존:` 줄)이 전부 [x] 또는 [~] 인** 첫 미완료 task 예요 +# (템플릿 펜스 안의 예시 줄을 집지 않게). 파서는 tasks-plan.sh 와 같은 goax_tasks_parse 예요. +# 미완료는 있는데 전부 의존에 막혔으면 status warning · result.blocked=true · blocked_tasks 에 막힌 task 와 +# 풀리지 않은 의존(unresolved — tasks.md 에 없는 ID 는 missing 에도)을 돌려줘요. 끝난 게 아니라 완료 게이트로 가면 안 돼요. # # Output (--json): # --task: {"status":"ok","result":{"spec":"…","task":"T013","tasks":["T013"],"state":"x","line":57,"lines":[57], # "changed":true,"changed_tasks":["T013"],"unchanged_tasks":[],"done_before":11,"done_after":12}} # (여러 개면 task 는 "T013,T014", line 은 첫 task 의 줄) -# --next: {"status":"ok","result":{"spec":"…","task":"T014","line":63,"text":"- [ ] T014 …"}} · 없으면 status skipped -# Exit: 0 ok · 1 error (task 없음 · ID 중복 · 락 실패 · 검증 불일치) · 2 skipped (--next 에서 미완료 없음) +# --next: {"status":"ok","result":{"spec":"…","task":"T014","line":63,"text":"- [ ] T014 …","deps":["T013"],"blocked":false}} +# · 미완료 없음 → status skipped (exit 2) +# · 전부 막힘 → {"status":"warning","result":{"spec":"…","task":null,"blocked":true, +# "blocked_tasks":[{"task":"T014","line":63,"text":"…","unresolved":["T013"],"missing":[]}]}} (exit 0) +# Exit: 0 ok (--next 의 전부 막힘 warning 포함) · 1 error (task 없음 · ID 중복 · 락 실패 · 검증 불일치) · 2 skipped (--next 에서 미완료 없음) set -euo pipefail SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -82,16 +88,56 @@ outside_fences() { } if [ "$MODE" = "next" ]; then - HIT=$(outside_fences | grep -m1 -E $'\t''- \[ \] ' || true) - if [ -z "$HIT" ]; then + # 그래프는 tasks-plan.sh 와 같은 파서(goax_tasks_parse)로 읽어요 — 둘이 다르게 읽으면 plan 은 blocked 라는데 + # --next 는 그 task 를 집어요. 의존이 전부 [x] 또는 [~] 인 첫 미완료 task 가 다음이에요. + PARSED=$(goax_tasks_parse "$TASKS") + if ! printf '%s\n' "$PARSED" | awk -F'|' '$2=="open"{f=1} END{exit !f}'; then if [ "$JSON_MODE" = true ]; then json_skip "미완료 task 없음 — §8 완료 게이트로"; fi goax_log "미완료 task 없음 — §8 완료 게이트로"; exit "$EXIT_SKIPPED" fi - LINE=${HIT%%$'\t'*}; TEXT=${HIT#*$'\t'} - ID=$(printf '%s' "$TEXT" | sed -E 's/^- \[ \] (\*\*|\[)?(T[0-9]+).*/\2/') + # 각 미완료 task → "ID|line|unresolved(,)|missing(,)" — unresolved 는 아직 [ ] 이거나 tasks.md 에 없는 의존 + GRAPH=$(printf '%s\n' "$PARSED" | awk -F'|' ' + NF { id[NR] = $1; st[NR] = $2; dp[NR] = $5; ln[NR] = $6; state[$1] = $2; n = NR } + END { + for (i = 1; i <= n; i++) { + if (st[i] != "open") continue + un = ""; ms = "" + k = split(dp[i], d, ",") + for (j = 1; j <= k; j++) { + if (d[j] == "") continue + if (!(d[j] in state)) { un = un (un == "" ? "" : ",") d[j]; ms = ms (ms == "" ? "" : ",") d[j] } + else if (state[d[j]] == "open") un = un (un == "" ? "" : ",") d[j] + } + printf "%s|%s|%s|%s\n", id[i], ln[i], un, ms + } + }') + HIT=$(printf '%s\n' "$GRAPH" | awk -F'|' 'NF && $3 == "" {print; exit}') + if [ -z "$HIT" ]; then + # 미완료는 있는데 전부 의존이 안 풀렸어요 — 끝난 게 아니라 막힌 거예요 (완료 게이트로 가면 안 돼요) + if [ "$JSON_MODE" = true ]; then + BLK=$(printf '%s\n' "$GRAPH" | while IFS='|' read -r bid bln bun bms; do + [ -n "$bid" ] || continue + btx=$(sed -n "${bln}p" "$TASKS") + jq -nc --arg t "$bid" --argjson l "$bln" --arg x "$btx" --arg u "$bun" --arg m "$bms" \ + 'def l: split(",") | map(select(length > 0)); {task:$t, line:$l, text:$x, unresolved:($u|l), missing:($m|l)}' + done | jq -sc .) + RESULT=$(jq -nc --arg spec "$SPEC" --argjson b "$BLK" '{spec:$spec, task:null, blocked:true, blocked_tasks:$b}') + MSG="미완료 task 가 전부 의존에 막혀 있어요 — $(printf '%s' "$BLK" | jq -r 'map("\(.task)←\(.unresolved|join(","))") | join(" · ")'). 의존 task 를 먼저 끝내거나, tasks.md 에 없는 의존(missing)은 고치세요" + json_output "warning" "$RESULT" "$MSG" "$(_goax_json_array "$MSG")" + else + goax_warn "미완료 task 가 전부 의존에 막혀 있어요:" + printf '%s\n' "$GRAPH" | awk -F'|' 'NF{printf " %s ← %s%s\n", $1, $3, ($4 != "" ? " (tasks.md 에 없음: " $4 ")" : "")}' >&2 + fi + exit "$EXIT_OK" + fi + IFS='|' read -r ID LINE _UN _MS < 0))), blocked:false}') json_output "ok" "$RESULT" "다음 task: $ID" else printf '%s\n' "$TEXT" diff --git a/templates/default/.ax/scripts/bash/rules-audit-scope.sh b/templates/default/.ax/scripts/bash/rules-audit-scope.sh new file mode 100755 index 0000000..18a73bc --- /dev/null +++ b/templates/default/.ax/scripts/bash/rules-audit-scope.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# .ax/scripts/bash/rules-audit-scope.sh — 룰 대조 리뷰(G7)의 범위: 변경 파일마다 걸린 룰 · 상시 룰 · 새 파일 +# +# Usage: +# bash rules-audit-scope.sh [--spec ] [--base ] [--files a,b,…] [--json] [--help] +# +# 왜 있나 — 편집 시점 훅은 룰 **경로**만 알려주고(spirit-rules-inject.sh · module-rules-inject.sh), +# rule-read-gate.sh 는 그 파일을 **읽었는지**만 봐요. 읽은 룰을 지켰는지, 새 파일이 이웃 관례(같은 레이어의 +# projection/·vo/·dto/ 같은 위치 · 이름 꼴)를 따랐는지는 아무도 대조하지 않았어요 (commerce 실측: `**/*.kt` +# 이름 룰과 `**/domain/**` 룰이 편집 때 안내됐는데도 위반이 완료 게이트를 통과했어요). +# 완료 직전에 새 컨텍스트의 rules-auditor 가 대조하고, 그 범위를 여기서 기계적으로 정해요 — +# "어떤 룰이 어느 파일에 걸리나" 를 리뷰어가 직접 grep 하면 빠뜨린 룰은 아무도 몰라요. +# +# 변경 파일 — --files 가 있으면 그것만. 없으면 git 에서: +# base(--base · 없으면 origin/HEAD → main → master 와의 merge-base)...HEAD + 워킹 트리(스테이징 포함) + 추적 안 되는 새 파일. +# `.ax/**` 는 빼요 (하네스 자신의 상태·문서예요 — 룰 파일의 ❌ 예시가 룰에 걸려요). +# 파일마다 걸린 룰 — 편집 훅과 **같은 매칭 함수**예요 (goax_rules_matching · goax_module_rules_matching). +# 훅과 다르게 매칭하면 "편집 때 안내한 룰" 과 "완료 때 대조한 룰" 이 갈라져요. +# 상시 룰 — Constitution(AGENTS.md, 없으면 CLAUDE.md) + `paths:` 가 없는 spirit 룰 파일 (경로를 안 가리고 늘 적용). +# 새 파일 — base 이후 추가됐거나 추적 안 되는 파일. 이웃 관례 대조의 대상이에요. +# +# Output (--json): +# {"status":"ok","result":{"spec":"…","base":"|null","review_file":".ax/docs/spec//review-rules.md", +# "header":"verdict: 진행 | 보강 필요","files":[{"path":"a/B.kt","new":true,"rules":[".ax/spirit/rules/naming.md"]}], +# "always":["AGENTS.md"],"rule_files":["…합집합…"],"new_files":["a/B.kt"]},…} +# 변경 파일이 0개면 status skipped (exit 2) — 대조할 게 없어요. +# Exit: 0 ok · 1 error (spec 없음 · --spec 모호 · git 아님인데 --files 도 없음) · 2 skipped (jq 없음 · 변경 0) + +set -euo pipefail +SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=common.sh +source "$SCRIPT_DIR/common.sh" + +JSON_MODE=false; SHOW_HELP=false; SPEC=""; BASE_ARG=""; FILES_ARG="" +while [ $# -gt 0 ]; do + case "$1" in + --json) JSON_MODE=true ;; + --dry-run) : ;; # 읽기 전용 + --spec) shift; SPEC="${1:-}" ;; + --base) shift; BASE_ARG="${1:-}" ;; + --files) shift; FILES_ARG="${1:-}" ;; + --help|-h) SHOW_HELP=true ;; + *) goax_error "unknown option: $1"; exit "$EXIT_ERROR" ;; + esac + shift +done +if [ "$SHOW_HELP" = true ]; then goax_help "${BASH_SOURCE[0]}"; exit "$EXIT_OK"; fi + +fail() { if [ "$JSON_MODE" = true ]; then json_error "$1"; fi; goax_error "$1"; exit "$EXIT_ERROR"; } +if ! command -v jq >/dev/null 2>&1; then + if [ "$JSON_MODE" = true ]; then json_skip "jq 가 필요해요"; fi + goax_warn "jq 가 없어 skip"; exit "$EXIT_SKIPPED" +fi + +PROJECT_ROOT=$(find_project_root) || exit "$EXIT_ERROR" +if [ -z "$SPEC" ] && [ -f "$PROJECT_ROOT/.ax/current-task.json" ]; then + SD=$(jq -r '.spec_dir // empty' "$PROJECT_ROOT/.ax/current-task.json" 2>/dev/null || true) + [ -n "$SD" ] && SPEC=$(basename "$SD") +fi +if [ -n "$SPEC" ]; then + RESOLVED=$(goax_resolve_spec "$SPEC" "$PROJECT_ROOT/.ax/docs/spec") && RC=0 || RC=$? + if [ "$RC" -eq 0 ]; then SPEC="$RESOLVED" + elif [ "$RC" -eq 2 ]; then fail "--spec '$SPEC' 이 여러 spec 에 걸려요: ${GOAX_SPEC_CANDIDATES} — 하나를 정확히 적으세요"; fi +fi +[ -n "$SPEC" ] && [ -d "$PROJECT_ROOT/.ax/docs/spec/$SPEC" ] || fail "spec 을 찾을 수 없어요: .ax/docs/spec/${SPEC:-<없음>}" +REVIEW_REL=".ax/docs/spec/$SPEC/review-rules.md" + +# ── 변경 파일 · 새 파일 ───────────────────────────────────────── +BASE=""; CHANGED=""; NEW="" +if [ -n "$FILES_ARG" ]; then + CHANGED=$(printf '%s' "$FILES_ARG" | tr ',' '\n' | sed -E 's/^[[:space:]]+|[[:space:]]+$//; s#^\./##') + # --files 로 받은 것 중 아직 추적 안 되는 파일은 새 파일이에요 (git 이 없으면 새 파일 판정은 비워요) + if git -C "$PROJECT_ROOT" rev-parse --git-dir >/dev/null 2>&1; then + NEW=$(printf '%s\n' "$CHANGED" | while IFS= read -r f; do + [ -n "$f" ] || continue + git -C "$PROJECT_ROOT" ls-files --error-unmatch -- "$f" >/dev/null 2>&1 || printf '%s\n' "$f" + done) + fi +else + git -C "$PROJECT_ROOT" rev-parse --git-dir >/dev/null 2>&1 \ + || fail "git 저장소가 아니에요 — --files a,b 로 변경 파일을 주세요" + if [ -n "$BASE_ARG" ]; then + BASE=$(git -C "$PROJECT_ROOT" merge-base HEAD "$BASE_ARG" 2>/dev/null) || fail "--base '$BASE_ARG' 를 HEAD 와 이을 수 없어요" + else + for cand in origin/HEAD main master; do + git -C "$PROJECT_ROOT" rev-parse --verify -q "$cand" >/dev/null 2>&1 || continue + BASE=$(git -C "$PROJECT_ROOT" merge-base HEAD "$cand" 2>/dev/null || true) + [ -n "$BASE" ] && break + done + fi + # 프로젝트 기준 상대 경로로 — 모노레포에선 git 루트와 프로젝트 루트가 달라요 (--relative) + G() { git -C "$PROJECT_ROOT" "$@" 2>/dev/null || true; } + CHANGED=$( { [ -n "$BASE" ] && G diff --relative --name-only --diff-filter=ACMR "$BASE" HEAD + G diff --relative --name-only --diff-filter=ACMR HEAD + G ls-files --others --exclude-standard; } | sort -u) + NEW=$( { [ -n "$BASE" ] && G diff --relative --name-only --diff-filter=A "$BASE" HEAD + G diff --relative --name-only --diff-filter=A HEAD + G ls-files --others --exclude-standard; } | sort -u) +fi +CHANGED=$(printf '%s\n' "$CHANGED" | grep -v '^\.ax/' | awk 'NF && !s[$0]++' || true) + +if [ -z "$CHANGED" ]; then + if [ "$JSON_MODE" = true ]; then json_skip "변경 파일이 없어요 (.ax/** 제외) — 대조할 게 없어요"; fi + goax_log "변경 파일 없음 — 대조할 게 없어요"; exit "$EXIT_SKIPPED" +fi + +# ── 파일마다 걸린 룰 — 편집 훅과 같은 함수 ──────────────────────── +SPIRIT_DIR="$PROJECT_ROOT/.ax/spirit/rules" +ROWS="" +while IFS= read -r f; do + [ -n "$f" ] || continue + rules=$( { goax_rules_matching "$SPIRIT_DIR" "$f" ".ax/spirit/rules/" + goax_module_rules_matching "$PROJECT_ROOT" "$f" | sed 's#^\(.*\)$#.ax/modules/\1/rules.md#'; } | awk 'NF && !s[$0]++' | paste -sd, -) + isnew=false; printf '%s\n' "$NEW" | grep -qxF -- "$f" && isnew=true + ROWS="${ROWS}${f}"$'\t'"${isnew}"$'\t'"${rules}"$'\n' +done < 0) | split("\t") | {path: .[0], new: (.[1] == "true"), + rules: ((.[2] // "") | split(",") | map(select(length > 0)))}] as $files + | ($always | split("\n") | map(select(length > 0))) as $al + | {spec: $spec, base: (if $base == "" then null else $base end), review_file: $rf, + header: "verdict: 진행 | 보강 필요", + files: $files, always: $al, + rule_files: (($files | map(.rules[]) + $al) | unique), + new_files: ($files | map(select(.new) | .path))}') + +N_FILES=$(printf '%s' "$RESULT" | jq '.files | length') +N_RULES=$(printf '%s' "$RESULT" | jq '.rule_files | length') +N_NEW=$(printf '%s' "$RESULT" | jq '.new_files | length') +NEXT="변경 ${N_FILES}개 · 대조할 룰 파일 ${N_RULES}개 · 새 파일 ${N_NEW}개 — 이 결과를 브리프로 rules-auditor 를 새 컨텍스트로 띄워 ${REVIEW_REL} 첫 줄에 verdict 를 받으세요" +if [ "$JSON_MODE" = true ]; then + json_output "ok" "$RESULT" "$NEXT" +else + printf '%s\n' "$RESULT" | jq -r '"base: \(.base // "없음 (워킹 트리만)")", + (.files[] | " \(.path)\(if .new then " (새 파일)" else "" end) ← \(if (.rules|length) > 0 then (.rules|join(", ")) else "경로 룰 없음" end)"), + "상시: \(.always | join(", "))", "리뷰 파일: \(.review_file)"' +fi +exit "$EXIT_OK" diff --git a/templates/default/.ax/scripts/bash/spec-review.sh b/templates/default/.ax/scripts/bash/spec-review.sh index 3769c45..572c6dc 100755 --- a/templates/default/.ax/scripts/bash/spec-review.sh +++ b/templates/default/.ax/scripts/bash/spec-review.sh @@ -8,6 +8,8 @@ # bash spec-review.sh [--spec ] --delta [--json] # 리뷰어가 본 본문 → 지금 본문 diff (재리뷰 브리프용) # bash spec-review.sh [--spec ] --fixup [--dry-run] [--json] # 통과 뒤의 오타·문구 수정을 리뷰 없이 받아들여요 # bash spec-review.sh [--spec ] --merge [--dry-run] [--json] # review-spec.md 합본 생성 (사람이 읽는 용) +# bash spec-review.sh [--spec ] --override --reason "<사용자 결정>" [--dry-run] [--json] +# # 상한에 닿은 뒤 사용자가 "이대로 진행" 을 정했을 때 — 지금 sha 에 승인 기록 # # 왜 필요한가 — 계획의 품질은 diff 시점이 아니라 계획 시점에 리뷰해야 올라가요. 그런데 리뷰어 # 둘(architect · evaluator)이 한 파일에 쓰면 (a) 첫 줄 verdict 하나로 "둘 다 진행" 을 표현할 수 @@ -20,6 +22,7 @@ # /review-spec.architect.md architect 가 직접 씀 — 1줄 `verdict: 진행|보강 필요|재논의 필요` · 2줄 `sha: <12자>` # /review-spec.evaluator.md evaluator 가 직접 씀 — 같은 형식 # /review-spec.md --merge 가 생성 (합본) +# /review-override.md --override 가 씀 — sha · by · at · stage/round · 두 verdict · reason (커밋해요 — 사용자 결정의 기록) # # sha 는 **spec.md + tasks.md 합산**이에요. spec.md 만 보면 리뷰 뒤에 tasks.md 를 통째로 # 갈아끼워도 pass 가 유지돼서, "L/XL 은 evaluator 가 tasks.md 까지 본다" 가 no-op 이 돼요. @@ -40,6 +43,7 @@ # required=none (size S) → pass. 리뷰 대상이 아니에요 # required=optional (size M) → 파일이 하나도 없으면 pass (선택). 하나라도 있으면 아래 규칙 # required=required (size L/XL) → 두 파일 모두 있고, 둘 다 `진행`, 둘 다 sha 가 현재(또는 fixup 으로 받아들인 것)와 같아야 pass +# review-override.md 의 sha 가 지금 sha 와 같으면 → pass (사용자 승인 — reason 에 by·at·사유) # sha 불일치 = 리뷰 뒤에 spec/tasks 가 바뀜 = 그쪽만 다시 받아요 (어느 파일인지 알려줘요) # # 필수 여부의 SSOT 는 tier-from-state.sh 의 `spec_review` 필드예요 (Size 축만: L/XL 필수 · M 선택 · S 없음). @@ -48,6 +52,11 @@ # 열어 두면 가장 큰 spec 이 가장 조용히 통과해요 — 그래서 모르면 닫는 쪽이에요. # 근거는 result 의 `required_source` 에 적어요. # +# --override 는 **라운드가 상한에 닿은 뒤**에만 돼요 (지금 단계의 라운드 ≥ 상한). 남은 차단이 기계적 수정뿐이라 +# 사용자가 "이대로 진행" 을 정했는데 리뷰를 한 바퀴 더 돌릴 수 없을 때, 그 결정을 **지금 sha** 에 묶어 파일로 남겨요. +# 예전엔 intent_notes 에 손으로 적었고 --status 는 그걸 몰라 pass=false 가 남았어요. 기록 뒤 본문이 바뀌면(sha 불일치) +# 승인은 더 이상 안 먹혀요 — 사용자가 본 본문이 아니니까요. 누가(git user.name · 없으면 $USER) · 언제 · 왜 를 적어요. +# # --dry-run 은 아무 파일도 안 써요 (--snapshot 의 .review-round · .review-snapshot, --fixup 의 .review-round, --merge 의 review-spec.md). # 라운드가 상한을 넘으면 status: warning 으로 알려요. 차단은 안 해요 — # 계속할지 멈출지는 skill 의 판단이라 exit 는 0 이에요. @@ -65,15 +74,17 @@ # base=reviewed 는 리뷰어가 본 본문 대비, base=snapshot 은 아직 아무도 안 본 스냅샷 대비예요 # (재리뷰 브리프에 붙일 것은 reviewed 쪽이에요 — snapshot 대비는 "리뷰 전에 더 고쳤다" 는 뜻) # --fixup {"status":"ok","result":{"reviewed_sha":"…","accepted_sha":"…","changed_lines":3,…}} +# --override {"status":"ok","result":{"sha":"…","by":"…","at":"…","reason":"…","stage":"spec","round":4,"max_rounds":3,"file":"…"}} +# --status 의 result 엔 "override":{"applied":bool,"sha":…,"by":…,"at":…,"reason":…} 가 붙어요 # -# Exit: 0 ok (pass 여부와 무관 — 판단은 caller) · 1 error (spec.md 없음 · --spec 모호 · --fixup 조건 미달) · 2 skipped (jq 없음) +# Exit: 0 ok (pass 여부와 무관 — 판단은 caller) · 1 error (spec.md 없음 · --spec 모호 · --fixup/--override 조건 미달) · 2 skipped (jq 없음) set -euo pipefail SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # shellcheck source=common.sh source "$SCRIPT_DIR/common.sh" -JSON_MODE=false; SHOW_HELP=false; DRY_RUN=false; MODE=""; SPEC=""; MAX_ROUNDS=""; STAGE_ARG="" +JSON_MODE=false; SHOW_HELP=false; DRY_RUN=false; MODE=""; SPEC=""; MAX_ROUNDS=""; STAGE_ARG=""; REASON_ARG="" while [ $# -gt 0 ]; do case "$1" in @@ -84,6 +95,8 @@ while [ $# -gt 0 ]; do --merge) MODE="merge" ;; --delta) MODE="delta" ;; --fixup) MODE="fixup" ;; + --override) MODE="override" ;; + --reason) shift; REASON_ARG="${1:-}" ;; --spec) shift; SPEC="${1:-}" ;; --stage) shift; STAGE_ARG="${1:-}" ;; --max-rounds) shift; MAX_ROUNDS="${1:-}" ;; @@ -138,6 +151,7 @@ E_FILE="$SPEC_DIR/review-spec.evaluator.md" ROUND_FILE="$SPEC_DIR/.review-round" SNAP_DIR="$SPEC_DIR/.review-snapshot" DELTA_FILE="$SNAP_DIR/delta.diff" +OVR_FILE="$SPEC_DIR/review-override.md" sha_file() { [ -f "$1" ] || { printf ''; return 0; } @@ -209,6 +223,12 @@ IFS='|' read -r E_P E_V E_S </dev/null | head -1 | sed -E 's/[[:space:]]+$//' || true; } +O_SHA=$(ovr_get sha); O_BY=$(ovr_get by); O_AT=$(ovr_get at); O_REASON=$(ovr_get reason) +OVR_APPLIED=false +[ -n "$O_SHA" ] && [ "$O_SHA" = "$SHA" ] && OVR_APPLIED=true + # "이 sha 를 누가 봤나" — 리뷰 파일 중 하나라도 그 sha 를 달고 있으면 본 거예요 reviewed_by_anyone() { [ -n "$1" ] && { [ "$A_S" = "$1" ] || [ "$E_S" = "$1" ]; }; } # "이 sha 를 둘 다 진행으로 봤나" — fixup 의 전제 @@ -275,7 +295,7 @@ case "$MODE" in OVER=false; WARN='[]'; WARN_MSG="" if [ "$ROUND" -gt "$MAX_ROUNDS" ]; then OVER=true - WARN_MSG="${NEW_STAGE} 단계 라운드 ${ROUND} — 상한 ${MAX_ROUNDS} 을 넘었어요. 리뷰를 더 돌리기보다 spec 을 다시 정의하거나 사용자 결정을 받으세요" + WARN_MSG="${NEW_STAGE} 단계 라운드 ${ROUND} — 상한 ${MAX_ROUNDS} 을 넘었어요. 리뷰를 더 돌리기보다 spec 을 다시 정의하거나 사용자 결정을 받으세요 (사용자가 이대로 진행을 정하면 --override --reason \"<결정>\")" command -v jq >/dev/null 2>&1 && WARN=$(_goax_json_array "$WARN_MSG") fi NOTE="" @@ -364,6 +384,39 @@ case "$MODE" in [ "$DRY_RUN" = true ] && printf ' (dry-run — .review-round 는 그대로예요)\n' fi ;; + override) + if [ "$REQUIRED" = none ]; then fail "합의 리뷰 대상이 아니에요 (${REQ_SRC}) — 승인할 리뷰가 없어요"; fi + REASON_ONE=$(printf '%s' "$REASON_ARG" | tr '\n\r' ' ' | sed -E 's/^[[:space:]]+|[[:space:]]+$//g') + [ -n "$REASON_ONE" ] || fail "--reason \"<사용자 결정>\" 이 필요해요 — 무엇을 왜 받아들였는지 한 줄 (예: \"남은 차단 2건은 오타·링크 — 사용자가 진행 결정\")" + [ "$ROUND" -gt 0 ] || fail "스냅샷이 없어요 — 리뷰를 한 번도 안 받았으면 승인할 게 없어요 (--snapshot 부터)" + OMAX="$MAX_ROUNDS"; if [ -z "$OMAX" ]; then if [ "$STAGE" = tasks ]; then OMAX=2; else OMAX=3; fi; fi + [ "$ROUND" -ge "$OMAX" ] || fail "${STAGE} 단계 라운드 ${ROUND}/${OMAX} — 상한 전에는 승인으로 넘기지 않아요. 지적을 반영하고 --snapshot 으로 다음 라운드를 받으세요" + BY=$(git -C "$PROJECT_ROOT" config user.name 2>/dev/null || true); BY="${BY:-${USER:-unknown}}" + AT=$(date -u +%Y-%m-%dT%H:%M:%SZ) + if [ "$DRY_RUN" != true ]; then + { + printf 'sha: %s\n' "$SHA" + printf 'by: %s\n' "$BY" + printf 'at: %s\n' "$AT" + printf 'stage: %s\n' "$STAGE" + printf 'round: %s/%s\n' "$ROUND" "$OMAX" + printf 'architect: %s (sha %s)\n' "${A_V:-없음}" "${A_S:-없음}" + printf 'evaluator: %s (sha %s)\n' "${E_V:-없음}" "${E_S:-없음}" + printf 'reason: %s\n' "$REASON_ONE" + } > "$OVR_FILE" + fi + NEXT="지금 sha ${SHA} 에 사용자 승인을 남겼어요 — --status 가 pass 예요. 본문을 또 고치면 승인은 풀려요 (사용자가 본 본문이 아니에요)" + if [ "$JSON_MODE" = true ]; then + RESULT=$(jq -nc --arg spec "$SPEC" --arg sha "$SHA" --arg by "$BY" --arg at "$AT" --arg r "$REASON_ONE" \ + --arg stage "$STAGE" --argjson round "$ROUND" --argjson maxr "$OMAX" --arg f "$OVR_FILE" --argjson dry "$DRY_RUN" \ + '{spec:$spec, sha:$sha, by:$by, at:$at, reason:$r, stage:$stage, round:$round, max_rounds:$maxr, + file:(if $dry then null else $f end), dry_run:$dry}') + json_output "ok" "$RESULT" "$NEXT" + else + printf 'spec %s — override: sha %s · %s · %s\n reason: %s\n' "$SPEC" "$SHA" "$BY" "$AT" "$REASON_ONE" + [ "$DRY_RUN" = true ] && printf ' (dry-run — %s 를 쓰지 않았어요)\n' "$OVR_FILE" + fi + ;; status|merge) FIXUP_APPLIED=false [ -n "$FIXUP_SHA" ] && [ "$FIXUP_SHA" = "$SHA" ] && FIXUP_APPLIED=true @@ -378,6 +431,8 @@ case "$MODE" in PASS=false; REASON="" if [ "$REQUIRED" = "none" ]; then PASS=true; REASON="합의 리뷰 대상이 아니에요 (${REQ_SRC})" + elif [ "$OVR_APPLIED" = true ]; then + PASS=true; REASON="사용자 승인 (override · ${O_BY:-?} · ${O_AT:-?}) — ${O_REASON:-사유 없음}. 리뷰 verdict: architect '${A_V:-없음}' · evaluator '${E_V:-없음}'" elif [ "$REQUIRED" = "optional" ] && [ "$A_P" = false ] && [ "$E_P" = false ]; then PASS=true; REASON="합의 리뷰는 선택이에요 (${REQ_SRC}) — 리뷰 없이 통과. --consensus 로 강제할 수 있어요" elif [ "$A_P" = false ] || [ "$E_P" = false ]; then @@ -401,6 +456,9 @@ case "$MODE" in { printf '# review-spec — %s (%s 단계 round %s · sha %s · %s)\n\n' "$SPEC" "$STAGE" "$ROUND" "$SHA" "$REQUIRED" printf 'pass: %s — %s\n\n' "$PASS" "$REASON" + if [ "$OVR_APPLIED" = true ]; then + printf '## 사용자 승인 (override)\n\n'; cat "$OVR_FILE"; printf '\n' + fi for who in architect evaluator; do f="$SPEC_DIR/review-spec.$who.md" printf '## %s\n\n' "$who" @@ -419,6 +477,7 @@ case "$MODE" in --argjson ep "$E_P" --arg ev "$E_V" --arg es "$E_S" --argjson em "$E_M" --arg ef "$E_FILE" \ --argjson pass "$PASS" --arg reason "$REASON" --arg mode "$MODE" --argjson dry "$DRY_RUN" \ --arg merged "${OUT:-}" \ + --argjson oa "$OVR_APPLIED" --arg os "$O_SHA" --arg ob "$O_BY" --arg oat "$O_AT" --arg orz "$O_REASON" \ '{spec:$spec, sha:$sha, spec_sha:$ssha, tasks_sha:(if $tsha=="" then null else $tsha end), snapshot_sha:(if $snap=="" then null else $snap end), stage:$stage, fixup:{applied:$fx, sha:(if $fxs=="" then null else $fxs end)}, @@ -427,6 +486,8 @@ case "$MODE" in changed_files:$ch, architect:{present:$ap, verdict:(if $av=="" then null else $av end), sha:(if $as=="" then null else $as end), sha_match:$am, file:$af}, evaluator:{present:$ep, verdict:(if $ev=="" then null else $ev end), sha:(if $es=="" then null else $es end), sha_match:$em, file:$ef}, + override:{applied:$oa, sha:(if $os=="" then null else $os end), by:(if $ob=="" then null else $ob end), + at:(if $oat=="" then null else $oat end), reason:(if $orz=="" then null else $orz end)}, pass:$pass, reason:$reason}') if [ "$PASS" = true ]; then json_output "ok" "$RESULT" "$REASON"; else json_output "warning" "$RESULT" "$REASON"; fi else @@ -435,6 +496,7 @@ case "$MODE" in printf ' evaluator %s\n' "$([ "$E_P" = true ] && printf '%s (sha %s%s)' "${E_V:-?}" "$E_S" "$([ "$E_M" = true ] && echo ' ✓' || echo ' ✗')" || echo '없음')" [ -n "$CHANGED_FILES" ] && printf ' 스냅샷 이후 변경: %s\n' "$CHANGED_FILES" [ "$FIXUP_APPLIED" = true ] && printf ' fixup: %s 를 리뷰된 %s 와 같은 것으로 받아들였어요\n' "$SHA" "$SNAP_SHA" + [ -n "$O_SHA" ] && [ "$OVR_APPLIED" != true ] && printf ' override 기록(sha %s)은 지금 본문과 달라 안 먹혀요 — 사용자에게 다시 확인받아요\n' "$O_SHA" printf ' pass: %s — %s\n' "$PASS" "$REASON" [ "$MODE" = "merge" ] && [ "$DRY_RUN" != true ] && printf ' 합본: %s\n' "$SPEC_DIR/review-spec.md" [ "$MODE" = "merge" ] && [ "$DRY_RUN" = true ] && printf ' (dry-run — 합본을 만들지 않았어요)\n' diff --git a/templates/default/.ax/scripts/bash/tasks-gate.sh b/templates/default/.ax/scripts/bash/tasks-gate.sh index 2714f11..d70e0e9 100755 --- a/templates/default/.ax/scripts/bash/tasks-gate.sh +++ b/templates/default/.ax/scripts/bash/tasks-gate.sh @@ -10,13 +10,15 @@ # 구분하지 못했어요. 실사용 리포에서 spec 21개 중 14개가 미완료 task 를 남긴 채 # 끝나 있었는데, 아무도 그걸 알려주지 않았어요. # -# 검사 6종: +# 검사 7종: # G1 미완료 `- [ ]` 가 남아 있나 (`- [~]` 보류는 제외 — 의도적 보류는 정상) # G2 커버리지 spec.md §3 의 AC 중 대응 task 가 없는 것 (spec-kit /analyze 의 coverage gap) # G3 orphan 어떤 AC 도 참조하지 않는 task # G4 유실 task 총 수가 봉인값보다 줄었나 — "미완료를 지워서 통과" 방어 # G5 원장 디스패치됐는데 보고가 없는 task · 보고 없이 [x] 가 된 task (lanes-dispatch.sh 필드) # G6 검증자 review.md **첫 줄**의 `verdict:` — 필수(size L 이상 · M×L3)인데 없거나 `진행` 이 아니면 미완료 +# G7 룰 대조 review-rules.md **첫 줄**의 `verdict:` — 필수 여부는 G6 과 같아요. rules-auditor 가 새 컨텍스트로 +# 변경 파일마다 걸린 룰(rules-audit-scope.sh)과 새 심볼의 이웃 관례를 대조해 써요 # # G2/G3 는 spec.md 에 `AC` ID 가 있을 때만 검사해요 (없는 기존 spec 은 생략). # G4 는 `.ax/state.json` 의 spec별 봉인값과 비교해요. 봉인값이 없으면 현재 수를 기록만 @@ -32,12 +34,15 @@ # # 왜 G5·G6 인가 — 체크박스를 채우는 쪽과 검사받는 쪽이 같으면 게이트가 아니라 자기보고예요. # G5 는 "보고를 받았는가" 를, G6 은 "다른 컨텍스트가 봤는가" 를 파일에서 확인해요. +# 왜 G7 인가 — 편집 훅은 룰 경로를 알려주고 rule-read-gate.sh 는 읽었는지만 봐요. 읽은 룰을 지켰는지, 새 파일이 +# 이웃 관례를 따랐는지는 G6(evaluator — spec·ADR 적합성)의 범위가 아니라 아무도 안 봤어요. 그래서 따로 받아요. # # Output (--json): # {"status":"ok|warning","result":{"spec":"012-x","total":12,"done":9,"open":2, # "paused":1,"ac_total":3,"ac_uncovered":["AC3"],"orphan_tasks":["T007"], # "task_count_drop":0,"dispatched_unreported":["T011"],"done_without_report":[], -# "review_required":true,"review_verdict":"진행","complete":false},...} +# "review_required":true,"review_verdict":"진행","rules_review_required":true,"rules_review_verdict":null, +# "complete":false},...} # --all 이면 result 는 {"specs":[위 객체 …],"spec_count":3,"total":…,"done":…,"open":…, # "complete":false,"violations":5} — 첫 spec 의 숫자에 전체 합계를 섞지 않아요. # @@ -97,11 +102,11 @@ fi # 펜스 밖 본문만 — 형식 설명 블록의 예시 task 를 실 task 로 세지 않도록 strip_fences() { awk '/^[[:space:]]*```/{f=!f; next} f{next} {print}' "$1" 2>/dev/null || true; } -# ── 한 spec 검사 → "spec|total|done|open|paused|ac_total|uncovered|orphans|drop|unreported|noreport|verdict|required" ── +# ── 한 spec 검사 → "spec|total|done|open|paused|ac_total|uncovered|orphans|drop|unreported|noreport|verdict|required|rverdict" ── check_spec() { local dir="$1" name; name=$(basename "$dir") local tasks="$dir/tasks.md" spec="$dir/spec.md" - [ -f "$tasks" ] || { printf '%s|0|0|0|0|0|||0|||%s|false\n' "$name" ""; return 0; } + [ -f "$tasks" ] || { printf '%s|0|0|0|0|0|||0|||%s|false|\n' "$name" ""; return 0; } local total done_n open_n paused IFS='|' read -r total done_n open_n paused </dev/null \ | sed -n 's/^verdict:[[:space:]]*//p' | sed -E 's/[[:space:]]+$//' || true) fi + # G7 룰 대조 — review-rules.md **첫 줄**. 쓰는 쪽은 rules-auditor 예요 (G6 과 같은 규칙) + local rverdict="" + if [ -f "$dir/review-rules.md" ]; then + rverdict=$(head -1 "$dir/review-rules.md" 2>/dev/null \ + | sed -n 's/^verdict:[[:space:]]*//p' | sed -E 's/[[:space:]]+$//' || true) + fi # size×risk 는 이 spec 을 맡은 작업에서 읽어요 — 지금 작업(current-task.json)이든 병렬 작업(.ax/tasks/*.json)이든. # 지금 작업만 보면 다른 세션이 새 작업을 여는 순간 이 spec 의 evaluator 필수 조건이 사라져요. if command -v jq >/dev/null 2>&1; then @@ -213,9 +224,9 @@ EOF fi fi - printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \ + printf '%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n' \ "$name" "$total" "$done_n" "$open_n" "$paused" "$ac_total" \ - "${uncovered% }" "${orphans% }" "$drop" "${unreported% }" "${noreport% }" "$verdict" "$required" + "${uncovered% }" "${orphans% }" "$drop" "${unreported% }" "${noreport% }" "$verdict" "$required" "$rverdict" } TARGETS="" @@ -243,7 +254,7 @@ $TARGETS EOF # 집계 + 보고 -# verdict 가 게이트를 막는가 — 필수인데 없거나, 무엇이든 `진행` 이 아니면 막아요 +# verdict 가 게이트를 막는가 — 필수인데 없거나, 무엇이든 `진행` 이 아니면 막아요 (G6 · G7 같은 규칙) review_blocks() { # $1 verdict · $2 required if [ "$2" = "true" ]; then [ "$1" = "진행" ] && return 1 || return 0 @@ -254,9 +265,9 @@ review_blocks() { # $1 verdict · $2 required FIRST=""; ROWS_X=""; SPEC_N=0 SUM_TOTAL=0; SUM_DONE=0; SUM_OPEN=0; SUM_PAUSED=0; ALL_COMPLETE=true -while IFS='|' read -r name total done_n open_n paused ac_total uncovered orphans drop unreported noreport verdict required; do +while IFS='|' read -r name total done_n open_n paused ac_total uncovered orphans drop unreported noreport verdict required rverdict; do [ -z "$name" ] && continue - [ -z "$FIRST" ] && FIRST="$name|$total|$done_n|$open_n|$paused|$ac_total|$uncovered|$orphans|$drop|$unreported|$noreport|$verdict|$required" + [ -z "$FIRST" ] && FIRST="$name|$total|$done_n|$open_n|$paused|$ac_total|$uncovered|$orphans|$drop|$unreported|$noreport|$verdict|$required|$rverdict" SPEC_N=$((SPEC_N + 1)) SUM_TOTAL=$((SUM_TOTAL + total)); SUM_DONE=$((SUM_DONE + done_n)) SUM_OPEN=$((SUM_OPEN + open_n)); SUM_PAUSED=$((SUM_PAUSED + paused)) @@ -268,9 +279,10 @@ while IFS='|' read -r name total done_n open_n paused ac_total uncovered orphans [ -n "$unreported" ] && V=$((V + 1)) [ -n "$noreport" ] && V=$((V + 1)) review_blocks "$verdict" "$required" && V=$((V + 1)) + review_blocks "$rverdict" "$required" && V=$((V + 1)) VIOLATIONS=$((VIOLATIONS + V)) C=true; [ "$V" -gt 0 ] && { C=false; ALL_COMPLETE=false; } - ROWS_X="${ROWS_X}${name}|${total}|${done_n}|${open_n}|${paused}|${ac_total}|${uncovered}|${orphans}|${drop}|${unreported}|${noreport}|${verdict}|${required}|${V}|${C}"$'\n' + ROWS_X="${ROWS_X}${name}|${total}|${done_n}|${open_n}|${paused}|${ac_total}|${uncovered}|${orphans}|${drop}|${unreported}|${noreport}|${verdict}|${required}|${V}|${C}|${rverdict}"$'\n' if [ "$JSON_MODE" != true ]; then printf '%s: %s/%s 완료' "$name" "$done_n" "$total" @@ -285,6 +297,10 @@ while IFS='|' read -r name total done_n open_n paused ac_total uncovered orphans [ -z "$verdict" ] && printf ' ⚠ evaluator 리뷰 필수 — review.md 첫 줄에 verdict 없음' \ || printf ' ⚠ evaluator verdict: %s' "$verdict" fi + if review_blocks "$rverdict" "$required"; then + [ -z "$rverdict" ] && printf ' ⚠ 룰 대조 필수 — review-rules.md 첫 줄에 verdict 없음' \ + || printf ' ⚠ 룰 대조 verdict: %s' "$rverdict" + fi printf '\n' fi done </dev/null 2>&1; then [ -n "$uncovered" ] && UNC_J=$(printf '%s' "$uncovered" | tr ' ' '\n' | jq -Rn '[inputs|select(length>0)]') [ -n "$orphans" ] && ORP_J=$(printf '%s' "$orphans" | tr ' ' '\n' | jq -Rn '[inputs|select(length>0)]') [ -n "$unreported" ] && UNR_J=$(printf '%s' "$unreported" | tr ' ' '\n' | jq -Rn '[inputs|select(length>0)]') [ -n "$noreport" ] && NOR_J=$(printf '%s' "$noreport" | tr ' ' '\n' | jq -Rn '[inputs|select(length>0)]') [ -n "$verdict" ] && VER_J=$(jq -n --arg v "$verdict" '$v') + [ -n "$rverdict" ] && RVER_J=$(jq -n --arg v "$rverdict" '$v') fi COMPLETE=false [ "$VIOLATIONS" -eq 0 ] && COMPLETE=true - RESULT=$(printf '{"spec":"%s","total":%s,"done":%s,"open":%s,"paused":%s,"ac_total":%s,"ac_uncovered":%s,"orphan_tasks":%s,"task_count_drop":%s,"dispatched_unreported":%s,"done_without_report":%s,"review_required":%s,"review_verdict":%s,"complete":%s,"violations":%s}' \ + RESULT=$(printf '{"spec":"%s","total":%s,"done":%s,"open":%s,"paused":%s,"ac_total":%s,"ac_uncovered":%s,"orphan_tasks":%s,"task_count_drop":%s,"dispatched_unreported":%s,"done_without_report":%s,"review_required":%s,"review_verdict":%s,"rules_review_required":%s,"rules_review_verdict":%s,"complete":%s,"violations":%s}' \ "${name:-}" "${total:-0}" "${done_n:-0}" "${open_n:-0}" "${paused:-0}" "${ac_total:-0}" \ - "$UNC_J" "$ORP_J" "${drop:-0}" "$UNR_J" "$NOR_J" "${required:-false}" "$VER_J" "$COMPLETE" "$VIOLATIONS") + "$UNC_J" "$ORP_J" "${drop:-0}" "$UNR_J" "$NOR_J" "${required:-false}" "$VER_J" "${required:-false}" "$RVER_J" "$COMPLETE" "$VIOLATIONS") if [ "$VIOLATIONS" -gt 0 ]; then if review_blocks "$verdict" "$required" && [ "${open_n:-0}" -eq 0 ]; then [ -z "$verdict" ] \ && json_output "warning" "$RESULT" "task 는 끝났지만 evaluator 리뷰가 필수예요 — 새 컨텍스트로 evaluator 를 띄워 review.md 첫 줄에 verdict 를 받으세요" \ || json_output "warning" "$RESULT" "evaluator verdict '${verdict}' — 지적을 task 로 옮겨 처리하거나 재논의하세요" + elif review_blocks "$rverdict" "$required" && [ "${open_n:-0}" -eq 0 ]; then + [ -z "$rverdict" ] \ + && json_output "warning" "$RESULT" "task 는 끝났지만 룰 대조가 필수예요 — rules-audit-scope.sh 로 범위를 뽑고 새 컨텍스트로 rules-auditor 를 띄워 review-rules.md 첫 줄에 verdict 를 받으세요" \ + || json_output "warning" "$RESULT" "룰 대조 verdict '${rverdict}' — review-rules.md 의 위반을 task 로 옮겨 고치고 rules-auditor 를 다시 띄우세요" elif [ -n "$unreported" ] || [ -n "$noreport" ]; then json_output "warning" "$RESULT" "레인 원장 불일치 — 산출물을 받고 lanes-dispatch.sh --report 로 기록한 뒤에만 체크박스를 켜세요" else diff --git a/templates/default/.ax/scripts/bash/tasks-plan.sh b/templates/default/.ax/scripts/bash/tasks-plan.sh index e52ea50..8c20030 100755 --- a/templates/default/.ax/scripts/bash/tasks-plan.sh +++ b/templates/default/.ax/scripts/bash/tasks-plan.sh @@ -5,7 +5,7 @@ # bash tasks-plan.sh [--spec ] [--json] [--help] # # 하는 일: -# ready 지금 바로 시작 가능한 task (미완료 + 의존이 전부 완료) +# ready 지금 바로 시작 가능한 task (미완료 + 의존이 전부 완료 [x] 또는 보류 [~]) # parallel 그중 `[P]` 가 붙었고 서로 파일이 안 겹치는 것 # violations `[P]` 인데 다른 `[P]` 와 파일이 겹치는 것 — 주장 검증 # @@ -73,58 +73,17 @@ if [ ! -f "$TASKS" ]; then goax_error "$NO_TASKS"; exit "$EXIT_ERROR" fi -# 파싱 — awk 한 번에. task 줄 + 뒤따르는 `의존:` 들여쓰기 줄을 같이 읽어요. -# 출력: "ID|state|P|files(,)|deps(,)" -PARSED=$(awk ' - function norm(p) { - gsub(/\/+/, "/", p) - while (sub(/^\.\//, "", p)) ; - while (sub(/\/\.\//, "/", p)) ; - sub(/\/+$/, "", p) - return p - } - function normlist(s, n, a, i, v, out) { - if (s == "") return "" - n = split(s, a, ",") - out = "" - for (i = 1; i <= n; i++) { v = norm(a[i]); if (v != "") out = (out == "" ? v : out "," v) } - return out - } - function flush() { - if (id != "") printf "%s|%s|%s|%s|%s\n", id, st, par, files, deps - id=""; st=""; par="0"; files=""; deps="" - } - /^[[:space:]]*```/ { fence = !fence; next } - fence { next } - /^- \[[ x~X]\] / { - flush() - line = $0 - st = "open" - if (line ~ /^- \[[xX]\]/) st = "done" - else if (line ~ /^- \[~\]/) st = "paused" - if (match(line, /\[P\]/)) par = "1" - if (match(line, /T[0-9][0-9][0-9]+/)) id = substr(line, RSTART, RLENGTH) - f = line - if (match(f, /files:[ ]*/)) { - f = substr(f, RSTART + RLENGTH) - gsub(/[ ]*,[ ]*/, ",", f); gsub(/^[ ]+|[ ]+$/, "", f) - files = normlist(f) - } - next - } - /^[ \t]+의존:/ { - d = $0; sub(/^[ \t]*의존:[ ]*/, "", d) - if (d !~ /없음|none|-$/) { gsub(/[ ]*,[ ]*/, ",", d); gsub(/^[ ]+|[ ]+$/, "", d); deps = d } - next - } - END { flush() } -' "$TASKS") +# 파싱 — common.sh 의 goax_tasks_parse (mark-task.sh --next 와 같은 파서). task 줄 + 뒤따르는 `의존:` 줄. +# 출력: "ID|state|P|files(,)|deps(,)|line" +PARSED=$(goax_tasks_parse "$TASKS") -DONE_IDS=$(printf '%s\n' "$PARSED" | awk -F'|' '$2=="done"{print $1}') +# 의존이 풀린 것 = [x] 완료 또는 [~] 의도적 보류 — mark-task.sh --next 와 같은 기준이에요. +# 보류를 안 풀린 걸로 세면 보류 task 에 기대는 task 가 영원히 blocked 인데, 완료 게이트(G1)는 보류를 정상으로 봐요. +DONE_IDS=$(printf '%s\n' "$PARSED" | awk -F'|' '$2=="done" || $2=="paused"{print $1}') is_done() { printf '%s\n' "$DONE_IDS" | grep -qx "$1"; } READY=""; BLOCKED=""; PCAND="" -while IFS='|' read -r id st par files deps; do +while IFS='|' read -r id st par files deps _ln; do [ -z "$id" ] && continue [ "$st" = "open" ] || continue ok=true diff --git a/templates/default/.ax/scripts/bash/tier-from-state.sh b/templates/default/.ax/scripts/bash/tier-from-state.sh index 208987d..16db0f8 100755 --- a/templates/default/.ax/scripts/bash/tier-from-state.sh +++ b/templates/default/.ax/scripts/bash/tier-from-state.sh @@ -17,7 +17,7 @@ # L × L3 → full (+ research/data-model/quickstart + ADR) # XL × * → full # -# evaluator (완료 시 새 컨텍스트 리뷰 — spec-implement 가 tasks-gate G6 으로 강제): +# evaluator (완료 시 새 컨텍스트 리뷰 — spec-implement 가 tasks-gate G6 으로 강제, 룰 대조 G7 도 같은 필수 여부): # S × * · M × L0~L2 → optional # M × L3 · L × * · XL × * → required # diff --git a/templates/default/.ax/scripts/bash/update-task.sh b/templates/default/.ax/scripts/bash/update-task.sh index 2ccc5f6..bdb44ab 100755 --- a/templates/default/.ax/scripts/bash/update-task.sh +++ b/templates/default/.ax/scripts/bash/update-task.sh @@ -17,6 +17,10 @@ # --task 이 작업만 갱신해요 (.ax/tasks/.json). 지금 작업(current-task.json 의 task_id)이 아니면 # current-task.json 은 그대로예요 — 병렬 작업이 서로 덮지 않아요. 없으면 지금 작업이 대상 # --activate --task 의 작업을 지금 작업으로 (current-task.json 의 task 필드를 그 작업 것으로 바꿔요) +# --follow-up --start 와 같이 — 이미 끝난(reset-task.sh 로 닫힌) spec 에 후속 작업을 새 작업으로 열어요. +# spec_id · spec_dir · spec_tier 를 그 spec 디렉토리(.tier)에서 이어받고 intent_notes.follow_up_of 에 +# spec 이름을 적어요. 같은 키를 --set 으로 주면 --set 이 이겨요. 닫힌 작업 파일은 reset 이 지워서 +# --activate 로는 못 돌아가요 — 무엇을 했는지는 git log · spec 이 원본이고, 이어 열 건 spec 이에요 # # 작업별 상태: task_id 가 있는 작업은 .ax/tasks/.json 이 원본이고 current-task.json 은 지금 작업의 # 사본 + handoff 예요. --start 로 다른 작업을 열면 앞 작업은 자기 파일에 남아요 (--task --activate 로 돌아가요). @@ -44,13 +48,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; ACTIVATE=false; TASK_OPT=""; NARGS=0; EMPTY_OPT=""; opt="" +PHASE=""; SETS='{}'; SET_KV=(); BLOCKED=""; INTENT=""; START=false; ACTIVATE=false; TASK_OPT=""; FOLLOW=""; 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|--task) + --phase|--set|--blocked-by|--merge-intent|--task|--follow-up) opt="$1" # 값이 비었거나 다음 토큰이 옵션(--…)이면 값을 삼키지 않고 오류로 — 예전엔 빈 값을 "안 준 것" 으로 접어 # updated_at 만 조용히 쓰고 ok 였고 (spec-validate 가 BLOCKED 를 비워 보내면 phase 만 spec_blocked 로 @@ -65,6 +69,7 @@ while [ $# -gt 0 ]; do --blocked-by) BLOCKED="$1" ;; --merge-intent) INTENT="$1" ;; --task) TASK_OPT="$1"; NARGS=$((NARGS - 1)) ;; # 대상만 고르고 바꿀 건 아니에요 + --follow-up) FOLLOW="$1" ;; esac fi NARGS=$((NARGS + 1)) ;; @@ -88,6 +93,7 @@ if ! command -v jq >/dev/null 2>&1; then fi [ "$NARGS" -gt 0 ] || fail "바꿀 것이 없어요 — --phase · --set · --blocked-by · --merge-intent · --start · --activate 중 하나는 줘요" [ "$ACTIVATE" = false ] || [ -n "$TASK_OPT" ] || fail "--activate 는 --task 와 같이 줘요 — 어느 작업을 지금 작업으로 할지요" +[ -z "$FOLLOW" ] || [ "$START" = true ] || fail "--follow-up 은 --start 와 같이 줘요 — 끝난 spec 의 후속은 새 작업이에요 (--set task_id=<새 id>)" # 작업 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}')" @@ -132,6 +138,21 @@ PROJECT_ROOT=$(find_project_root) || exit "$EXIT_ERROR" FILE="$PROJECT_ROOT/.ax/current-task.json"; REL=".ax/current-task.json" [ -f "$FILE" ] || fail "$REL 이 없어요 — /up 으로 설치를 마쳐요" +# --follow-up — 끝난 spec 의 spec_* 를 이어받아요. 디렉토리가 SSOT 예요 (닫힌 작업 파일은 reset 이 지웠어요) +if [ -n "$FOLLOW" ]; then + FSPEC=$(goax_resolve_spec "$FOLLOW" "$PROJECT_ROOT/.ax/docs/spec") && FRC=0 || FRC=$? + [ "$FRC" -ne 2 ] || fail "--follow-up '$FOLLOW' 이 여러 spec 에 걸려요: ${GOAX_SPEC_CANDIDATES} — 하나를 정확히 적어요" + [ "$FRC" -eq 0 ] && [ -f "$PROJECT_ROOT/.ax/docs/spec/$FSPEC/spec.md" ] \ + || fail "--follow-up '$FOLLOW' 의 spec 을 찾을 수 없어요 (.ax/docs/spec//spec.md)" + FTIER=$(awk -F: '/^[[:space:]]*tier:/{v=$2; gsub(/[[:space:]]/,"",v); print v; exit}' "$PROJECT_ROOT/.ax/docs/spec/$FSPEC/.tier" 2>/dev/null || true) + case "$FTIER" in standard|full) ;; *) FTIER="" ;; esac + FID=$(printf '%s\n' "$FSPEC" | goax_doc_key) + SETS=$(jq -nc --argjson o "$SETS" --arg id "$FID" --arg dir ".ax/docs/spec/$FSPEC" --arg t "$FTIER" \ + '{spec_id: $id, spec_dir: $dir} + (if $t != "" then {spec_tier: $t} else {} end) + $o') # --set 이 이겨요 + [ "$INTENT" = null ] && INTENT='{}' + INTENT=$(jq -nc --argjson o "$INTENT" --arg s "$FSPEC" '{follow_up_of: $s} + $o') +fi + # 바뀌는 키 목록 — 출력용. updated_at 은 항상. CHANGED=$(jq -nc --arg p "$PHASE" --argjson s "$SETS" --argjson bb "$BLOCKED" --argjson it "$INTENT" --argjson st "$START" \ '[ (if $p != "" then "phase" else empty end), ($s | keys[]), @@ -178,7 +199,7 @@ resolve_target() { 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 + ERRMSG="작업 ${TARGET} 이 없어요 ($TASK_REL) — 새 작업은 triage 가 --start 로 열어요. reset-task.sh 로 끝낸 작업의 spec 에 후속을 이으려면 --start --follow-up --set task_id=<새 id>"; return 1 fi return 0 } diff --git a/tests/smoke.sh b/tests/smoke.sh index b587fb4..1156b83 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -585,7 +585,7 @@ for s in common next-spec-num tier-from-state init-spec-dir add-spec-files \ check-spec-clarity slug-from-text check-templates-drift check-manifest-install promote-mistake \ check-rule-enforcement check-sensor-liveness \ build-memory spirit-lint rules-index doctor-scan status-note update-task update-state constitution-apply \ - tasks-plan tasks-gate lanes-hotfiles lanes-dispatch spec-review \ + tasks-plan tasks-gate lanes-hotfiles lanes-dispatch spec-review rules-audit-scope \ zero-init zero-probe zero-verify zero-domain-risk design-caps screen-measure screen-report-check; do f="$SCRIPTS_DIR/$s.sh" if [ -f "$f" ]; then @@ -1631,6 +1631,7 @@ echo '{"phase":"implementing","spec_dir":".ax/docs/spec/012-x","size":"L","risk" tg | jq -e '.result.review_required == true and .result.review_verdict == null and .result.complete == false' >/dev/null 2>&1 \ && pass "tasks-gate G6 — L×L1 은 evaluator 필수, review.md 없으면 미완료" || fail "tasks-gate G6 — 필수 리뷰 부재를 통과시킴" printf 'verdict: 진행\n\n## Evaluator Review\n발견 0건\n' > "$TG/.ax/docs/spec/012-x/review.md" +printf 'verdict: 진행\n' > "$TG/.ax/docs/spec/012-x/review-rules.md" # G7 도 같은 필수 여부 — §65 가 따로 봐요 tg | jq -e '.result.review_verdict == "진행" and .result.complete == true' >/dev/null 2>&1 \ && pass "tasks-gate G6 — verdict 진행 → complete" || fail "tasks-gate G6 — verdict 진행을 못 읽음" printf 'verdict: 보강 필요\n' > "$TG/.ax/docs/spec/012-x/review.md" @@ -5320,6 +5321,138 @@ for ag in lane-worker lane-scout; do || fail "$ag — 외부 쓰기 금지나 객체 형식 종료 응답이 빠졌어요" done +section "65. commerce 마찰 다섯 — --next 의존 · 관례 위치 · 룰 대조 G7 · 후속 작업 · 합의 리뷰 승인" +# ─────────────────────────────────────────────────────────── +SB="$REPO/templates/default/.ax/scripts/bash" +# (1) mark-task --next 는 `의존:` 을 봐요 — tasks-plan 과 같은 파서(goax_tasks_parse) +NX=$(mktemp -d); mkdir -p "$NX/.ax/scripts/bash" "$NX/.ax/docs/spec/2026-09-25-ab12-x" +cp "$SB/"{common,mark-task,tasks-plan}.sh "$NX/.ax/scripts/bash/" +NXF="$NX/.ax/docs/spec/2026-09-25-ab12-x/tasks.md" +cat > "$NXF" <<'NXE' +``` +- [ ] T001 예시 +``` +- [x] T001 a +- [ ] T002 b + 의존: T003 +- [~] T004 보류 +- [ ] T003 c + 의존: T001, T041 (아직 없는 task) +- [ ] T005 d + 의존: T004 (보류) +NXE +nx() { (cd "$NX" && bash .ax/scripts/bash/mark-task.sh --spec 2026-09-25-ab12-x "$@" 2>/dev/null); } +nxp() { (cd "$NX" && bash .ax/scripts/bash/tasks-plan.sh --spec 2026-09-25-ab12-x --json 2>/dev/null); } +NX1=$(nx --next --json) +{ [ "$(printf '%s' "$NX1" | jq -r '.result.task')" = T005 ] && [ "$(printf '%s' "$NX1" | jq -c '.result.deps')" = '["T004"]' ] \ + && nxp | jq -e '.result.ready == ["T005"] and .result.blocked == ["T002","T003"]' >/dev/null; } \ + && pass "mark-task --next — 의존이 [ ] 인 T002·T003 을 건너뛰고 [~] 의존인 T005 를 골라요 (tasks-plan 과 같은 답)" \ + || fail "mark-task --next 의존 무시: $NX1 / plan $(nxp | jq -c .result)" +sed -i.bak 's/^- \[ \] T005/- [x] T005/' "$NXF" && rm -f "$NXF.bak" +NX2=$(nx --next --json); NXR=$? +{ [ "$NXR" = 0 ] && printf '%s' "$NX2" | jq -e '.status == "warning" and .result.task == null and .result.blocked == true + and (.result.blocked_tasks | map(.task)) == ["T002","T003"] + and (.result.blocked_tasks[1].unresolved == ["T041"]) and (.result.blocked_tasks[1].missing == ["T041"])' >/dev/null; } \ + && pass "mark-task --next — 미완료가 전부 막히면 warning · blocked_tasks · 풀리지 않은 의존(없는 ID 는 missing) — 완료로 안 읽혀요" \ + || fail "mark-task --next 전부 막힘 판정: rc=$NXR $NX2" +printf -- '- [ ] T009 e\n 의존: 결제 배포\n' >> "$NXF" +nxp | jq -e '.result.blocked | index("T009")' >/dev/null \ + && pass "goax_tasks_parse — T-ID 없는 의존 표기는 '의존 없음' 으로 풀리지 않아요" || fail "T-ID 없는 의존이 ready 로 풀렸어요" +grep -q 'files: 목록과 레포 관례' "$REPO/skills/spec-implement/SKILL.md" && grep -q '같은 레이어 이웃을 grep' "$REPO/skills/spec-tasks/SKILL.md" \ + && grep -q '소유 목록과 레포 관례가 충돌' "$REPO/agents/lane-worker.md" && grep -q '소유 목록과 레포 관례가 충돌' "$REPO/skills/lane/SKILL.md" \ + && pass "관례 위치 — spec-tasks 이웃 grep · spec-implement 위임 브리프 · lane 브리프 · lane-worker 가 충돌 시 멈추고 보고" \ + || fail "files: 와 관례 위치 충돌 규칙이 spec-tasks · spec-implement · lane · lane-worker 중 어딘가에 없어요" +rm -rf "$NX" + +# (3) 룰 대조 — rules-audit-scope 는 편집 훅과 같은 매칭, G7 은 G6 과 같은 필수 여부 +RA=$(mktemp -d); mkdir -p "$RA/.ax/scripts/bash" "$RA/.ax/spirit/rules" "$RA/.ax/docs/spec/2026-09-25-cd34-y" "$RA/src/domain" +cp "$SB/"{common,rules-audit-scope,tasks-gate,tier-from-state}.sh "$RA/.ax/scripts/bash/" +printf -- '---\npaths:\n - "**/*.kt"\n---\n## SP-NAME-001: 이름\n' > "$RA/.ax/spirit/rules/naming.md" +printf -- '---\npaths:\n - "**/domain/**"\n---\n## SP-DOM-001: 도메인\n' > "$RA/.ax/spirit/rules/domain.md" +printf -- '---\ncategory: ops\n---\n## SP-OPS-001: 상시\n' > "$RA/.ax/spirit/rules/ops.md" +echo '# c' > "$RA/AGENTS.md" +git -C "$RA" init -q 2>/dev/null; git -C "$RA" add -A 2>/dev/null +git -C "$RA" -c user.email=a@b -c user.name=a commit -qm init 2>/dev/null +echo x > "$RA/src/domain/OrderProjection.kt"; echo y > "$RA/.ax/docs/spec/2026-09-25-cd34-y/note.md" +RAJ=$(GOAX_PROJECT_DIR="$RA" bash "$RA/.ax/scripts/bash/rules-audit-scope.sh" --spec cd34 --json 2>/dev/null) +printf '%s' "$RAJ" | jq -e '.result.files == [{"path":"src/domain/OrderProjection.kt","new":true,"rules":[".ax/spirit/rules/domain.md",".ax/spirit/rules/naming.md"]}] + and .result.always == ["AGENTS.md",".ax/spirit/rules/ops.md"] and .result.new_files == ["src/domain/OrderProjection.kt"] + and (.result.review_file | endswith("2026-09-25-cd34-y/review-rules.md"))' >/dev/null 2>&1 \ + && pass "rules-audit-scope — 변경 파일마다 걸린 룰 · 상시 룰(Constitution · paths 없는 룰) · 새 파일, .ax/** 는 빼요" \ + || fail "rules-audit-scope 범위: $RAJ" +cp "$REPO/templates/default/.ax/current-task.json.template" "$RA/.ax/current-task.json"; echo '{}' > "$RA/.ax/state.json" +jq '.task_id="W"|.phase="implementing"|.spec_dir=".ax/docs/spec/2026-09-25-cd34-y"|.size="L"|.risk="L2"' "$RA/.ax/current-task.json" > "$RA/ct" && mv "$RA/ct" "$RA/.ax/current-task.json" +RD="$RA/.ax/docs/spec/2026-09-25-cd34-y" +printf '## 3.\n- **AC1** a\n' > "$RD/spec.md"; printf -- '- [x] T001 [AC1] a — files: src/domain/OrderProjection.kt\n' > "$RD/tasks.md" +echo 'verdict: 진행' > "$RD/review.md" +rg() { GOAX_PROJECT_DIR="$RA" bash "$RA/.ax/scripts/bash/tasks-gate.sh" --spec cd34 --json 2>/dev/null; } +G7A=$(rg | jq -c '[.result.complete, .result.rules_review_required, .result.rules_review_verdict]') +echo 'verdict: 보강 필요' > "$RD/review-rules.md"; G7B=$(rg | jq -c '[.result.complete, .result.rules_review_verdict]') +echo 'verdict: 진행' > "$RD/review-rules.md"; G7C=$(rg | jq -c '[.result.complete]') +{ [ "$G7A" = '[false,true,null]' ] && [ "$G7B" = '[false,"보강 필요"]' ] && [ "$G7C" = '[true]' ]; } \ + && pass "tasks-gate G7 — 필수(L×L2)인데 review-rules.md 없거나 보강 필요면 미완료, 진행이면 완료" \ + || fail "tasks-gate G7: 없음=$G7A 보강=$G7B 진행=$G7C" +{ grep -q 'review-rules.md' "$REPO/agents/rules-auditor.md" && grep -q '^verdict: 진행 | 보강 필요' "$REPO/agents/rules-auditor.md" \ + && grep -q '시작 전 필수' "$REPO/agents/rules-auditor.md" && grep -q 'rules-auditor' "$REPO/agents/evaluator.md" \ + && grep -q '8.1.5 룰 대조' "$REPO/skills/spec-implement/SKILL.md" && grep -q 'rules-audit-scope.sh' "$REPO/skills/spec-implement/SKILL.md" \ + && grep -q 'rules-auditor' "$REPO/templates/default/.ax/hooks/subagent-start/harness-pointer.sh"; } \ + && pass "rules-auditor — review-rules.md 첫 줄 verdict · evaluator 는 관례 대조를 넘겨요 · spec-implement §8.1.5 · 포인터 훅 제외" \ + || fail "rules-auditor 배선이 빠졌어요 (agent · evaluator 하지 않는 것 · spec-implement §8.1.5 · harness-pointer)" +# 편집 훅은 경로와 함께 룰 제목도 줘요 (세션당 처음 매칭될 때만) +mkdir -p "$RA/.ax/hooks/pre-edit"; cp "$REPO/templates/default/.ax/hooks/pre-edit/spirit-rules-inject.sh" "$RA/.ax/hooks/pre-edit/" +RI=$(echo "{\"tool_input\":{\"file_path\":\"$RA/src/domain/OrderProjection.kt\"},\"session_id\":\"s65\"}" \ + | CLAUDE_PROJECT_DIR="$RA" bash "$RA/.ax/hooks/pre-edit/spirit-rules-inject.sh" 2>/dev/null | jq -r '.hookSpecificOutput.additionalContext') +printf '%s' "$RI" | grep -q 'SP-DOM-001: 도메인' && printf '%s' "$RI" | grep -q 'SP-NAME-001: 이름' \ + && pass "spirit-rules-inject — 룰 파일 경로와 함께 룰 제목(SP-…: …)을 줘요" || fail "spirit-rules-inject 제목 누락: $RI" +rm -rf "$RA" + +# (4) 끝난 spec 의 후속 작업 — --start --follow-up 이 spec_* 를 이어받아요 +FU=$(mktemp -d); mkdir -p "$FU/.ax/scripts/bash" "$FU/.ax/docs/spec/2026-09-25-ef56-pay" +cp "$SB/"{common,update-task,tier-from-state,reset-task}.sh "$FU/.ax/scripts/bash/" +cp "$REPO/templates/default/.ax/current-task.json.template" "$FU/.ax/current-task.json" +echo s > "$FU/.ax/docs/spec/2026-09-25-ef56-pay/spec.md"; printf 'tier: full\n' > "$FU/.ax/docs/spec/2026-09-25-ef56-pay/.tier" +fu() { GOAX_PROJECT_DIR="$FU" bash "$FU/.ax/scripts/bash/update-task.sh" "$@" --json 2>/dev/null; } +fu --start --phase implementing --set task_id=A --set spec_dir=.ax/docs/spec/2026-09-25-ef56-pay >/dev/null +GOAX_PROJECT_DIR="$FU" bash "$FU/.ax/scripts/bash/reset-task.sh" --json >/dev/null 2>&1 +FUA=$(fu --task A --activate | jq -r '.errors[0] // ""') +FUB=$(fu --follow-up ef56 --phase tasks | jq -r .status) +fu --start --follow-up ef56 --phase tasks --set task_id=B >/dev/null +{ printf '%s' "$FUA" | grep -q -- '--follow-up' && [ "$FUB" = error ] \ + && [ "$(jq -c '[.task_id,.phase,.spec_id,.spec_dir,.spec_tier,.intent_notes.follow_up_of]' "$FU/.ax/current-task.json")" \ + = '["B","tasks","2026-09-25-ef56",".ax/docs/spec/2026-09-25-ef56-pay","full","2026-09-25-ef56-pay"]' ] \ + && [ -f "$FU/.ax/tasks/B.json" ] && [ "$(fu --start --follow-up nope --set task_id=C | jq -r .status)" = error ]; } \ + && pass "update-task --start --follow-up — 닫힌 작업 대신 새 작업이 spec_id·spec_dir·spec_tier·follow_up_of 를 이어받아요 (--start 없이·없는 spec 은 거부)" \ + || fail "update-task --follow-up: activate=$FUA nostart=$FUB ct=$(jq -c . "$FU/.ax/current-task.json")" +grep -q -- '--follow-up' "$REPO/skills/triage/SKILL.md" && pass "triage — 끝난 spec 후속 작업 사용법 한 줄" || fail "triage 에 --follow-up 안내 없음" +rm -rf "$FU" + +# (5) 합의 리뷰 상한 도달 뒤 사용자 승인 — --override 는 지금 sha 에 기록, --status 가 인정 +OV=$(mktemp -d); mkdir -p "$OV/.ax/scripts/bash" "$OV/.ax/docs/spec/2026-09-25-aa11-z" +cp "$SB/"{common,spec-review,tier-from-state}.sh "$OV/.ax/scripts/bash/" +OD="$OV/.ax/docs/spec/2026-09-25-aa11-z"; echo 'v1' > "$OD/spec.md"; printf 'tier: full\n' > "$OD/.tier" +git -C "$OV" init -q 2>/dev/null; git -C "$OV" config user.name tester65 +ov() { GOAX_PROJECT_DIR="$OV" bash "$OV/.ax/scripts/bash/spec-review.sh" --spec aa11 "$@" --json 2>/dev/null; } +ov_round() { # 스냅샷 → 두 리뷰어 파일 (architect 보강 필요) + local sha; sha=$(ov --snapshot | jq -r .result.sha) + printf 'verdict: 보강 필요\nsha: %s\n' "$sha" > "$OD/review-spec.architect.md" + printf 'verdict: 진행\nsha: %s\n' "$sha" > "$OD/review-spec.evaluator.md" +} +ov_round; echo v2 > "$OD/spec.md"; ov_round +OVE=$(ov --override --reason "x" | jq -r .status) # 라운드 2/3 — 상한 전 +echo v3 > "$OD/spec.md"; ov_round +OVN=$(ov --override | jq -r .status) # --reason 없음 +echo 'v3 오타' > "$OD/spec.md" +ov --override --reason "남은 지적은 오타 — 사용자 진행 결정" --dry-run >/dev/null; OVDRY=$([ -f "$OD/review-override.md" ] && echo wrote || echo none) +ov --override --reason "남은 지적은 오타 — 사용자 진행 결정" >/dev/null +OVS=$(ov --status | jq -c '[.result.pass, .result.override.applied, .result.override.by]') +echo v4 > "$OD/spec.md"; OVS2=$(ov --status | jq -c '[.result.pass, .result.override.applied]') +{ [ "$OVE" = error ] && [ "$OVN" = error ] && [ "$OVDRY" = none ] && [ "$OVS" = '[true,true,"tester65"]' ] && [ "$OVS2" = '[false,false]' ] \ + && grep -q '^reason: 남은 지적은 오타' "$OD/review-override.md" && grep -q '^at: ' "$OD/review-override.md"; } \ + && pass "spec-review --override — 상한 전·사유 없음은 거부, dry-run 무기록, 지금 sha 에만 pass (누가·언제·사유), 본문이 바뀌면 풀려요" \ + || fail "spec-review --override: 상한전=$OVE 사유없음=$OVN dry=$OVDRY status=$OVS 변경후=$OVS2" +grep -q -- '--override --reason' "$REPO/skills/spec-validate/SKILL.md" && pass "spec-validate — 상한 절에 --override 사용법" || fail "spec-validate 상한 절에 --override 없음" +rm -rf "$OV" + section "✨ 결과" # ─────────────────────────────────────────────────────────── if [ "$fail_count" -eq 0 ]; then From ac0245acbbd087e147c14c45f315caa2b8bfc8fe Mon Sep 17 00:00:00 2001 From: stone Date: Tue, 6 Oct 2026 20:37:44 +0900 Subject: [PATCH 2/4] =?UTF-8?q?docs(harness):=20=EA=B4=80=EB=A1=80=20?= =?UTF-8?q?=EC=9C=84=EC=B9=98=20=EC=98=88=EC=8B=9C=EB=A5=BC=20=EC=8A=A4?= =?UTF-8?q?=ED=83=9D=20=EC=A4=91=EB=A6=BD=EC=9C=BC=EB=A1=9C=20=E2=80=94=20?= =?UTF-8?q?=ED=8A=B9=EC=A0=95=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EB=94=94=EB=A0=89=ED=86=A0=EB=A6=AC=C2=B7=EC=96=B8=EC=96=B4=20?= =?UTF-8?q?=EC=98=88=EC=8B=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit projection/·vo/·dto/, **/*.kt, commerce 실측 언급을 '같은 레이어의 별도 하위 디렉토리' 같은 범용 표현으로 바꿔요. 플러그인 문구는 특정 스택·프로젝트를 전제하지 않아요. Co-Authored-By: Claude Opus 5.5 --- agents/evaluator.md | 2 +- agents/lane-worker.md | 2 +- agents/rules-auditor.md | 4 ++-- changelog/0.7.10.md | 6 +++--- skills/lane/SKILL.md | 2 +- skills/spec-implement/SKILL.md | 4 ++-- skills/spec-tasks/SKILL.md | 6 +++--- templates/default/.ax/scripts/bash/rules-audit-scope.sh | 4 ++-- tests/smoke.sh | 2 +- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/agents/evaluator.md b/agents/evaluator.md index 40d7bac..26c0534 100644 --- a/agents/evaluator.md +++ b/agents/evaluator.md @@ -44,7 +44,7 @@ Generator/Evaluator 모델 — Generator의 self-praise bias 제거. ## 하지 않는 것 — 구현 리뷰 -- **이름·패키지 관례 대조** — 변경 파일에 걸린 spirit·모듈 룰과 새 파일의 위치(`projection/`·`vo/`·`dto/` 같은 +- **이름·패키지 관례 대조** — 변경 파일에 걸린 spirit·모듈 룰과 새 파일의 위치(같은 레이어 이웃 디렉토리)·이름 꼴·분리를 이웃과 맞춰 보는 건 `rules-auditor` 의 범위예요 (G7, `review-rules.md`). 여기서도 하면 같은 지적이 두 파일에 와요. 보이면 `### 종합` 에 "룰 대조 범위" 로 한 줄만 적어요 diff --git a/agents/lane-worker.md b/agents/lane-worker.md index 41dae96..e21455f 100644 --- a/agents/lane-worker.md +++ b/agents/lane-worker.md @@ -35,7 +35,7 @@ description: "실행 레인 sub-agent — tasks.md 를 파일 소유권으로 다른 세션이 그 파일을 이미 고쳤을 수 있고, 커밋 메시지는 새 안을 말하는데 바이트는 옛 안인 사고가 실제로 났어요 - **소유 목록과 레포 관례가 충돌할 때 목록 안 파일에 끼워 넣기** — 새 타입을 만들어야 하는데 이웃 관례가 - 별도 파일·별도 위치(같은 레이어의 `projection/`·`vo/`·`dto/` 등)인데 그 경로가 소유 목록에 없으면, 소유 파일 + 별도 파일·별도 위치(같은 레이어의 하위 디렉토리 등)인데 그 경로가 소유 목록에 없으면, 소유 파일 안에 중첩 선언하지 말고 멈추고 보고해요 — `관례 충돌 — <새 타입>, 이웃 관례 <경로 2개>, 필요한 파일 <경로>`. 목록을 늘리는 건 코디네이터가 해요. 끼워 넣으면 목록은 지켰지만 레포 관례를 어긴 코드가 남아요 - **커밋** — 워킹 트리에 변경만 남겨요. 합치는 건 한 사람이 해요 diff --git a/agents/rules-auditor.md b/agents/rules-auditor.md index dbad140..361c48c 100644 --- a/agents/rules-auditor.md +++ b/agents/rules-auditor.md @@ -36,8 +36,8 @@ description: "완료 직전 룰 대조 sub-agent — spec-implement §8 이 새 1. **룰 대조** — `rule_files` 를 전부 Read 해요. 파일마다 걸린 룰(`files[].rules`) + 상시 룰(`always`)의 각 `## SP-…` · `AX:…` 항목을 그 파일의 diff 와 대조해요. 위반이면 `<파일>:<줄>` · 룰 토큰 · 룰 문장 한 줄을 적어요. 2. **이웃 관례 대조** — `new_files` 와, 기존 파일에 새로 생긴 클래스·함수·타입마다: - - **위치** — 같은 레이어의 이웃 디렉토리를 `ls`·`grep` 해요. 이웃이 `projection/` · `vo/` · `dto/` 같은 하위 디렉토리에 - 같은 종류를 모아 두는데 새 것만 다른 곳(예: 쓰는 파일 안에 중첩 선언)에 있으면 위반이에요 + - **위치** — 같은 레이어의 이웃 디렉토리를 `ls`·`grep` 해요. 이웃이 같은 종류를 별도 하위 디렉토리에 + 모아 두는데 새 것만 다른 곳(예: 쓰는 파일 안에 중첩 선언)에 있으면 위반이에요 - **이름 꼴** — 이웃의 접두·접미(`*Command` · `*Response` · `*Impl` …)와 다르면 위반이에요 - **분리** — 이웃은 한 파일에 한 타입인데 새 것만 여러 타입을 한 파일에 넣었으면 위반이에요 근거는 늘 이웃 파일 경로 2개 이상이에요. 이웃이 한 개뿐이거나 서로 다르면 관례가 아니라 비차단에 적어요. diff --git a/changelog/0.7.10.md b/changelog/0.7.10.md index d24f569..2111371 100644 --- a/changelog/0.7.10.md +++ b/changelog/0.7.10.md @@ -2,16 +2,16 @@ ## 무엇이 좋아졌나 — 누구에게 체감되나 -> spec 을 tasks 로 나눠 구현하는 사람에게 체감돼요. 다섯 가지 모두 commerce 레포에서 실제로 겪은 마찰이에요. +> spec 을 tasks 로 나눠 구현하는 사람에게 체감돼요. 다섯 가지 모두 실사용 프로젝트에서 실제로 겪은 마찰이에요. - **다음 task 가 의존을 지켜요.** `mark-task.sh --next` 가 `의존:` 을 무시하고 펜스 밖 첫 미완료 task 를 골라, 아직 안 끝난 task 에 기대는 일을 먼저 시작했어요. 이제 의존이 전부 `[x]`·`[~]` 인 첫 task 를 골라요 (`tasks-plan.sh` 와 같은 파서). 미완료가 전부 막혀 있으면 "끝났다" 가 아니라 `warning` 과 막힌 task · 풀리지 않은 의존(없는 ID 는 따로)을 돌려줘요 — 완료 게이트로 잘못 넘어가지 않아요. -- **새 파일이 관례 위치로 가요.** `files:` 목록이 관례 위치(같은 레이어의 `projection/`·`vo/`·`dto/`)를 빠뜨리면, +- **새 파일이 관례 위치로 가요.** `files:` 목록이 관례 위치(같은 레이어의 별도 하위 디렉토리)를 빠뜨리면, 구현자가 목록을 지키려고 새 타입을 목록 안 파일에 끼워 넣었어요. spec-tasks 는 새 파일 경로를 적기 전에 이웃을 grep 하고, 레인·위임 브리프는 "목록과 관례가 충돌하면 멈추고 보고" 를 실어요. -- **완료 전에 룰을 대조해요 (G7).** 위반된 룰(이름 `**/*.kt`, 도메인 `**/domain/**`)은 편집 때 훅이 실제로 +- **완료 전에 룰을 대조해요 (G7).** 위반된 룰(이름 룰, 레이어 룰)은 편집 때 훅이 실제로 매칭해서 안내했어요. 그런데 안내 훅은 룰 **경로**만 주고, `rule-read-gate.sh` 는 그 파일을 **읽었는지**만 봐요 — 읽은 룰을 지켰는지, 새 파일이 이웃 관례를 따랐는지는 아무도 대조하지 않았어요. 이제 새 컨텍스트의 `rules-auditor` 가 변경 파일마다 걸린 룰과 새 심볼의 이웃 관례를 대조해 `review-rules.md` 첫 줄에 verdict 를 쓰고, diff --git a/skills/lane/SKILL.md b/skills/lane/SKILL.md index 268956d..22c0b2a 100644 --- a/skills/lane/SKILL.md +++ b/skills/lane/SKILL.md @@ -264,7 +264,7 @@ bash .ax/scripts/bash/tasks-plan.sh --spec "$SPEC" --json - **공유 이름을 바꾸면 즉시 보고하세요.** props 이름·타입 축·토큰 이름을 바꾸는 건 파일 안의 일이 아니라 레인 밖의 일이에요. - **소유 목록과 레포 관례가 충돌하면 멈추고 보고하세요.** 새 타입의 이웃 관례가 별도 파일·별도 - 위치(같은 레이어의 `projection/`·`vo/`·`dto/` 등)인데 그 경로가 목록에 없으면, 목록 안 파일에 + 위치(같은 레이어의 하위 디렉토리 등)인데 그 경로가 목록에 없으면, 목록 안 파일에 끼워 넣지 말고 `관례 충돌 — <새 타입>, 이웃 관례 <경로 2개>, 필요한 파일 <경로>` 로 보고해요. 목록은 코디네이터가 늘려요. - **보고는 한 번, 한 경로로.** 도구 목록에 `SendMessage` 가 있는지로 경로가 정해져요. diff --git a/skills/spec-implement/SKILL.md b/skills/spec-implement/SKILL.md index 81ca034..b8c7566 100644 --- a/skills/spec-implement/SKILL.md +++ b/skills/spec-implement/SKILL.md @@ -195,8 +195,8 @@ RECURRENCE=$(grep -lE "^category:.*\\b${TASK_DOMAIN}\\b" .ax/mistakes/*.md 2>/de 이 세션이 task 하나를 서브에이전트(executor 등)에게 맡기면, 브리프에 그 task 의 `files:` 목록과 함께 이 줄을 넣어요: ``` -files: 목록과 레포 관례(새 파일 위치·분리)가 충돌하면 — 예: 새 타입의 이웃 관례는 같은 레이어 projection/·vo/·dto/ -같은 별도 파일인데 그 경로가 목록에 없으면 — 목록 안 파일에 끼워 넣지 말고 멈추고 보고하세요 +files: 목록과 레포 관례(새 파일 위치·분리)가 충돌하면 — 예: 새 타입의 이웃 관례는 같은 레이어의 별도 +하위 디렉토리·별도 파일인데 그 경로가 목록에 없으면 — 목록 안 파일에 끼워 넣지 말고 멈추고 보고하세요 (`관례 충돌 — <새 타입>, 이웃 관례 <경로 2개>, 필요한 파일 <경로>`). ``` diff --git a/skills/spec-tasks/SKILL.md b/skills/spec-tasks/SKILL.md index 86941f6..22f84db 100644 --- a/skills/spec-tasks/SKILL.md +++ b/skills/spec-tasks/SKILL.md @@ -131,9 +131,9 @@ task 가 있으면 그건 spec 에 없는 일을 하고 있다는 신호예요. 규칙: - **모든 task 에 `files:` 필수** — 겹침 검사가 이 값으로만 성립해요 (`lanes-hotfiles.sh` 의 `tasks_missing_files` 로 사각지대 확인) - **새 파일을 만드는 task 는 `files:` 를 적기 전에 같은 레이어 이웃을 grep 해요** — 이웃이 같은 종류를 - `projection/`·`vo/`·`dto/` 같은 하위 디렉토리에 따로 두면 새 파일 경로도 거기로 정해요. `files:` 는 구현자에겐 - "이 밖은 손대지 마" 라서, 관례 위치가 빠진 목록을 받은 구현자는 새 타입을 목록 안 파일에 끼워 넣어요 (commerce 실측). - 예: `ls $(dirname <비슷한 기존 파일>)/` · `grep -rl "interface .*Projection" <모듈>/` 으로 이웃 2개 이상을 확인 + 별도 하위 디렉토리에 따로 두면 새 파일 경로도 거기로 정해요. `files:` 는 구현자에겐 + "이 밖은 손대지 마" 라서, 관례 위치가 빠진 목록을 받은 구현자는 새 타입을 목록 안 파일에 끼워 넣어요. + 예: `ls $(dirname <비슷한 기존 파일>)/` · `grep -rl "<같은 종류의 이름 꼴>" <모듈>/` 으로 이웃 2개 이상을 확인 - Phase 순서: setup → foundational → user stories(priority순) → polish - TaskID는 sortable (T001, T010, T100) - `[P]` 는 기본값이 아니라 증명될 때만 — 판단이 서지 않으면 붙이지 않아요 diff --git a/templates/default/.ax/scripts/bash/rules-audit-scope.sh b/templates/default/.ax/scripts/bash/rules-audit-scope.sh index 18a73bc..fe5431c 100755 --- a/templates/default/.ax/scripts/bash/rules-audit-scope.sh +++ b/templates/default/.ax/scripts/bash/rules-audit-scope.sh @@ -6,8 +6,8 @@ # # 왜 있나 — 편집 시점 훅은 룰 **경로**만 알려주고(spirit-rules-inject.sh · module-rules-inject.sh), # rule-read-gate.sh 는 그 파일을 **읽었는지**만 봐요. 읽은 룰을 지켰는지, 새 파일이 이웃 관례(같은 레이어의 -# projection/·vo/·dto/ 같은 위치 · 이름 꼴)를 따랐는지는 아무도 대조하지 않았어요 (commerce 실측: `**/*.kt` -# 이름 룰과 `**/domain/**` 룰이 편집 때 안내됐는데도 위반이 완료 게이트를 통과했어요). +# 위치 · 이름 꼴)를 따랐는지는 아무도 대조하지 않았어요 (실측: 편집 때 이름 룰과 레이어 룰이 +# 안내됐는데도 위반이 완료 게이트를 통과했어요). # 완료 직전에 새 컨텍스트의 rules-auditor 가 대조하고, 그 범위를 여기서 기계적으로 정해요 — # "어떤 룰이 어느 파일에 걸리나" 를 리뷰어가 직접 grep 하면 빠뜨린 룰은 아무도 몰라요. # diff --git a/tests/smoke.sh b/tests/smoke.sh index 1156b83..6f699bf 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -5321,7 +5321,7 @@ for ag in lane-worker lane-scout; do || fail "$ag — 외부 쓰기 금지나 객체 형식 종료 응답이 빠졌어요" done -section "65. commerce 마찰 다섯 — --next 의존 · 관례 위치 · 룰 대조 G7 · 후속 작업 · 합의 리뷰 승인" +section "65. 실사용 마찰 다섯 — --next 의존 · 관례 위치 · 룰 대조 G7 · 후속 작업 · 합의 리뷰 승인" # ─────────────────────────────────────────────────────────── SB="$REPO/templates/default/.ax/scripts/bash" # (1) mark-task --next 는 `의존:` 을 봐요 — tasks-plan 과 같은 파서(goax_tasks_parse) From 256d6664cf3614c954f2df35e9eea328a43f35e4 Mon Sep 17 00:00:00 2001 From: stone Date: Tue, 6 Oct 2026 20:42:48 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix(tasks):=20'=EC=9D=98=EC=A1=B4:=20?= =?UTF-8?q?=EC=97=86=EC=9D=8C=20(=EC=84=A4=EB=AA=85)'=20=EC=9D=B4=20?= =?UTF-8?q?=EC=98=81=EC=9B=90=ED=9E=88=20=EB=A7=89=ED=9E=88=EB=8D=98=20?= =?UTF-8?q?=ED=9A=8C=EA=B7=80=EB=A5=BC=20=EA=B3=A0=EC=B3=90=EC=9A=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit goax_tasks_parse 가 없음·none·- 를 정확히 일치로만 받아, 뒤에 설명을 붙인 '의존: 없음 (첫 task)' 를 풀리지 않는 의존으로 읽었어요. 그 task 와 뒤 task 가 전부 blocked 가 되고 --next 가 전부 막힘 warning 을 냈어요. 예전 파서는 의존 없음으로 읽었으니 업그레이드 회귀예요. 값 뒤 공백·괄호부터는 설명으로 봐요. T-ID 없는 다른 표기(오타)는 여전히 막혀요. smoke §65 에 케이스 추가. lane SKILL 의 낡은 tasks-plan.sh:83 참조도 goax_tasks_parse 로. Co-Authored-By: Claude Opus 5.5 --- changelog/0.7.10.md | 2 +- skills/lane/SKILL.md | 2 +- templates/default/.ax/scripts/bash/common.sh | 7 ++++--- tests/smoke.sh | 4 ++++ 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/changelog/0.7.10.md b/changelog/0.7.10.md index 2111371..e35e1cb 100644 --- a/changelog/0.7.10.md +++ b/changelog/0.7.10.md @@ -27,7 +27,7 @@ ## 바뀐 것 - `common.sh` `goax_tasks_parse` — tasks.md 실행 그래프 파서 하나 (`tasks-plan.sh` · `mark-task.sh --next` 공용). - `의존:` 줄은 T-ID 만 읽고, T-ID 가 없는 다른 표기는 풀리지 않는 의존으로 남겨요 (오타가 "의존 없음" 으로 통과하지 않게) + `의존:` 줄은 T-ID 만 읽고, T-ID 가 없는 다른 표기는 풀리지 않는 의존으로 남겨요 (오타가 "의존 없음" 으로 통과하지 않게). `없음 (첫 task)` 처럼 없음·none·- 뒤에 설명을 붙인 건 의존 없음이에요 - `tasks-plan.sh` — 의존이 `[~]` 보류여도 풀린 걸로 봐요 (`--next`·G1 과 같은 기준) - `mark-task.sh --next` — 의존 판정 · 전부 막힘이면 `status: warning` · `result.blocked` · `blocked_tasks[]` - `rules-audit-scope.sh` (신규) — 변경 파일마다 걸린 spirit·모듈 룰(편집 훅과 같은 매칭 함수) · 상시 룰 · 새 파일 diff --git a/skills/lane/SKILL.md b/skills/lane/SKILL.md index 22c0b2a..e5c47d6 100644 --- a/skills/lane/SKILL.md +++ b/skills/lane/SKILL.md @@ -362,7 +362,7 @@ bash .ax/scripts/bash/status-note.sh --show --json | jq -r '.result.sections.ren ## 13. 알려진 제약 — 이걸 모르면 게이트가 조용히 헛돌아요 -- **`tasks-plan.sh` 는 의존 키워드를 한글 `의존:` 로 하드코딩해요** (`tasks-plan.sh:83`). +- **`tasks-plan.sh` 는 의존 키워드를 한글 `의존:` 로 하드코딩해요** (`common.sh` 의 `goax_tasks_parse`). `deps:`·`depends:` 는 안 읽혀요. "의존 없음"으로 인정되는 값은 `없음`·`none`·`-` 뿐이에요. 다른 표기는 그 이름의 task 에 의존하는 걸로 읽혀서 영원히 blocked 가 돼요. - **파일 겹침 검출은 task 줄에 파일 경로가 실제로 적혀 있어야 성립해요.** `files:` 가 diff --git a/templates/default/.ax/scripts/bash/common.sh b/templates/default/.ax/scripts/bash/common.sh index 825b167..24d8dd6 100755 --- a/templates/default/.ax/scripts/bash/common.sh +++ b/templates/default/.ax/scripts/bash/common.sh @@ -1516,7 +1516,7 @@ goax_resolve_spec() { # 출력: "ID|state|P|files(,)|deps(,)|line" state = open|done|paused · P = 0|1 · line = task 줄 번호 # - ``` 코드펜스 안은 건너뛰어요 (템플릿의 형식 설명 예시가 실 task 로 세지 않게) # - files: 경로는 정규화해요 (`./` 제거 · 중복 `/` · 끝 `/`) — 표기 차이로 겹침 검사를 빠져나갈 수 없어요 -# - deps 는 task 다음 들여쓰기 `의존:` 줄의 T-ID 만 뽑아요 — `없음`·`none`·`-` 은 빈 값, +# - deps 는 task 다음 들여쓰기 `의존:` 줄의 T-ID 만 뽑아요 — `없음`·`none`·`-` 은 빈 값(뒤에 설명이 붙어도), # `T001 (스키마)` 처럼 설명이 붙어도 T001 만 남아요. T-ID 가 하나도 없는 다른 표기는 원문 그대로 남겨 # (없는 task 에 의존한 것처럼) 풀리지 않게 해요 — 오타가 "의존 없음" 으로 조용히 통과하지 않게요 goax_tasks_parse() { @@ -1564,9 +1564,10 @@ goax_tasks_parse() { deps = (deps == "" ? v : deps "," v) } # T-ID 가 없는데 없음/none/- 도 아니면 읽을 수 없는 의존 — 조용히 "의존 없음" 으로 풀지 않고 - # 그대로 남겨요 (tasks.md 에 없는 ID 처럼 영원히 안 풀려서 눈에 띄어요) + # 그대로 남겨요 (tasks.md 에 없는 ID 처럼 영원히 안 풀려서 눈에 띄어요). + # `없음 (첫 task)` · `none — 독립` 처럼 값 뒤에 설명을 붙인 건 의존 없음이에요 (예전 파서도 그렇게 읽었어요) gsub(/^[ \t]+|[ \t]+$/, "", raw) - if (deps == "" && raw !~ /^(없음|none|-)?$/) { gsub(/[|,]/, ";", raw); deps = raw } + if (deps == "" && raw !~ /^((없음|none|-)([[:space:](].*)?)?$/) { gsub(/[|,]/, ";", raw); deps = raw } next } END { flush() } diff --git a/tests/smoke.sh b/tests/smoke.sh index 6f699bf..1f628b1 100644 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -5358,6 +5358,10 @@ NX2=$(nx --next --json); NXR=$? printf -- '- [ ] T009 e\n 의존: 결제 배포\n' >> "$NXF" nxp | jq -e '.result.blocked | index("T009")' >/dev/null \ && pass "goax_tasks_parse — T-ID 없는 의존 표기는 '의존 없음' 으로 풀리지 않아요" || fail "T-ID 없는 의존이 ready 로 풀렸어요" +printf -- '- [ ] T010 f\n 의존: 없음 (첫 task)\n- [ ] T011 g\n 의존: none — 독립\n- [ ] T012 h\n 의존: -\n' >> "$NXF" +nxp | jq -e '(.result.ready | index("T010") and index("T011") and index("T012")) and (.result.blocked | index("T009"))' >/dev/null \ + && pass "goax_tasks_parse — '없음 (설명)' · 'none — 설명' · '-' 은 의존 없음 (T-ID 없는 다른 표기만 막혀요)" \ + || fail "설명이 붙은 '없음' 이 막혔어요: $(nxp | jq -c .result)" grep -q 'files: 목록과 레포 관례' "$REPO/skills/spec-implement/SKILL.md" && grep -q '같은 레이어 이웃을 grep' "$REPO/skills/spec-tasks/SKILL.md" \ && grep -q '소유 목록과 레포 관례가 충돌' "$REPO/agents/lane-worker.md" && grep -q '소유 목록과 레포 관례가 충돌' "$REPO/skills/lane/SKILL.md" \ && pass "관례 위치 — spec-tasks 이웃 grep · spec-implement 위임 브리프 · lane 브리프 · lane-worker 가 충돌 시 멈추고 보고" \ From 59b669ee313527ad8a80b6b8688337b8b7781634 Mon Sep 17 00:00:00 2001 From: stone Date: Tue, 6 Oct 2026 21:03:10 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix(evals):=20doctor-i6=20scaffold=20?= =?UTF-8?q?=EA=B0=80=20=ED=85=9C=ED=94=8C=EB=A6=BF=20=EC=95=88=EB=82=B4=20?= =?UTF-8?q?=EC=A4=84=20=EC=9D=B4=EB=AA=A8=EC=A7=80(=E2=9A=A0=E2=86=92?= =?UTF-8?q?=E2=9D=97)=20=EB=B3=80=EA=B2=BD=20=EB=92=A4=20=EA=B9=A8?= =?UTF-8?q?=EC=A0=B8=20=EC=9E=88=EB=8D=98=20=EA=B2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AGENTS.md.template 의 '> ⚠ hook 파일을…' 이 #23 에서 '> ❗' 로 바뀌었는데 scaffold 는 옛 문자열을 index 해서 ValueError 로 케이스 전체가 실행 전에 실패했어요. 이모지 대신 문구로 찾고 그 줄 처음부터 잘라요. Co-Authored-By: Claude Opus 5.5 --- evals/doctor-i6/scaffold.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/evals/doctor-i6/scaffold.sh b/evals/doctor-i6/scaffold.sh index 07e170e..a4dd0b8 100755 --- a/evals/doctor-i6/scaffold.sh +++ b/evals/doctor-i6/scaffold.sh @@ -25,7 +25,8 @@ python3 - <<'PY' import re p='AGENTS.md'; s=open(p).read() start=s.index('🔴 **`AX:CRITICAL:001`**') -end=s.index('> ⚠ hook 파일을 아직 못 만들었으면') +# 안내 줄의 이모지는 바뀌어요(⚠ → ❗) — 문구로 찾고 그 줄 처음부터 잘라요 +end=s.rindex('\n', 0, s.index('hook 파일을 아직 못 만들었으면')) + 1 rule=('🔴 **`EVL:CRITICAL:001`** 사용자 노출 카피 안전선을 어기지 않는다\n' '- enforced_by: external:vitest\n' '- enforced_kind: test\n\n')