Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ if command -v node >/dev/null 2>&1; then
# has forbidden these for a while with nothing enforcing it, which is how
# md5sum reached translator and broke image downloads on macOS (issue #172).
node "$repo_root/scripts/check-shell-portability.mjs"
# The portability guard is a detector, and this repo's own lore says a
# detector without coverage reports "nothing wrong" when it means "could
# not look". Nine holes across three review rounds, each found by hand;
# these cases are that verification kept.
node "$repo_root/scripts/check-shell-portability.test.mjs"
# Skill-prose progressive-disclosure smell detector — INFORMATIONAL ONLY. It exits 0
# regardless, but the `|| true` keeps it non-blocking even if node itself errors.
node "$repo_root/scripts/check-skill-prose.mjs" || true
Expand Down
25 changes: 22 additions & 3 deletions .github/workflows/validate-codex.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ jobs:
# downloaded image to one filename on macOS (issue #172). Flags a construct
# only when no fallback, capability probe, or `# portability-ok:` sits near it.
- run: node scripts/check-shell-portability.mjs
# Both directions: constructs that must be flagged, and correct fallbacks
# that must not be. The second half is what keeps the guard usable -- one
# false positive on `stat -c … || stat -f …` and it gets switched off.
- run: node scripts/check-shell-portability.test.mjs

# The BSD fallbacks the suite is full of can only be proven on a BSD userland.
# Without this leg every `stat -c || stat -f` and `date -d || date -j` branch is
Expand All @@ -61,11 +65,26 @@ jobs:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
# Printed directly rather than through `echo "$(...)"`. Command substitution
# inside echo discards the inner status -- echo succeeds either way -- which
# is the "종료 상태가 사라지는 자리" P1 this repo added in #183, and a workflow
# that violates its own rule is a bad exemplar. The step's shell is `bash -e`,
# so a genuinely missing bash now fails here instead of printing a blank line.
# The loop keeps its `|| echo '(absent)'`: absence is the answer it is asking.
- name: runner userland (informational)
run: |
echo "bash on PATH: $(which -a bash | tr '\n' ' ')"
echo "PATH bash: $(bash --version | head -1)"
echo "/bin/bash: $(/bin/bash --version | head -1)"
# The step shell is `bash -e`, which does NOT set pipefail: a failing
# `/bin/bash --version` was masked by `head`'s success and the step
# still passed -- the same false green this block was rewritten to
# remove, one layer down. Verified: `bash -e -c '/missing/bash
# --version | head -1; echo survived'` exits 0.
set -o pipefail
echo "bash on PATH:"
which -a bash
echo "PATH bash:"
bash --version | head -1
echo "/bin/bash:"
/bin/bash --version | head -1

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 진단 파이프라인에 pipefail을 적용하세요

이 macOS 진단 스텝의 기본 bash -e에는 pipefail이 없으므로, PATH의 bash는 존재하지만 /bin/bash가 없거나 실행에 실패하는 환경에서 /bin/bash --version의 실패가 head의 성공 상태로 덮여 스텝이 계속 통과합니다. 실제로 bash -e -c '/missing/bash --version | head -1; echo survived'가 0으로 끝나므로, 이번 변경이 제거하려던 false-green이 그대로 남습니다. 파이프 전에 set -o pipefail을 적용하거나 버전 명령의 상태를 별도로 확인해야 합니다.

AGENTS.md reference: AGENTS.md:L293-L293

Useful? React with 👍 / 👎.

for t in sed grep date stat jq md5sum timeout gsed gdate gstat; do
printf '%-8s %s\n' "$t" "$(command -v "$t" || echo '(absent)')"
done
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ node scripts/check-shell-portability.mjs

`check-doc-consistency.mjs` 는 README 구조 트리·`## Plugins` 표·문서에 박힌 카운트 문자열을 marketplace.json 과 대조하고 `.githooks/pre-commit` 에서 차단한다. 문서 블록이 관례가 아니라 기계적으로 강제된다는 뜻이므로, 어떤 문서 내용을 "코드에서 재생성 가능하니 지워도 된다" 고 판단하기 전에 훅이 그 블록을 요구하는지 먼저 확인한다. 가드 6종 전체 설명은 `README.md` 의 "CI 가드가 지키는 것".

`check-shell-portability.mjs` 는 `code_review.md` P1 의 크로스플랫폼 규칙을 기계적으로 강제한다 — GNU 전용 구문(`md5sum`·`sed -i`·`grep -P`·`date -d`·`stat -c`·`timeout`·`${VAR,,}`·`mapfile`·`declare -A` 등)이 **폴백도 capability probe 도 없이** 쓰인 경우만 잡는다. 정상 폴백 쌍(`stat -c … || stat -f …`)과 probe 분기는 통과하며, 증거는 **코드여야 하고 주석은 인정하지 않는다** (대체재를 언급하는 주석이 실제 폴백으로 계수되면 이 가드가 존재하는 이유인 `md5sum` 자체가 새어나간다). 정말 예외인 줄은 `# portability-ok: <사유>` 로 표시한다. `.llmwiki/`·`.claude/spec/`·`code_review.md`·`AGENTS.md` 는 그 구문을 *설명*할 뿐이라 스캔에서 제외된다.
`check-shell-portability.mjs` 는 `code_review.md` P1 의 크로스플랫폼 규칙을 기계적으로 강제한다 — GNU 전용 구문(`md5sum`·`sed -i`·`grep -P`·`date -d`·`stat -c`·`timeout`·`${VAR,,}`·`mapfile`·`declare -A` 등)이 **폴백도 capability probe 도 없이** 쓰인 경우만 잡는다. 정상 폴백 쌍(`stat -c … || stat -f …`)은 통과한다 — 단 **`||` 는 우변에 BSD 대응물이 있을 때만** 폴백으로 인정한다 (`md5sum "$f" || exit 1` 은 여전히 macOS 에서 깨지므로 통과시키지 않는다). capability probe 도 **대응물과 짝일 때만** 인정한다 — 짝 없는 `sed --version` 뒤의 `sed -i` 는 분기가 없으므로 잡는다. BSD 대응물이 아예 없는 구문(`grep -P`·`timeout`·bash 4 문법 등)은 가리킬 포터블 대안이 없으므로 `||` 나 probe 로 지워지지 않고 명시적 예외만 받는다. GNU 긴 옵션 별칭(`--perl-regexp`·`--date`·`--format`·`--in-place`)도 같은 규칙으로 잡는다. 증거는 **코드여야 하고 주석은 인정하지 않는다** (대체재를 언급하는 주석이 실제 폴백으로 계수되면 이 가드가 존재하는 이유인 `md5sum` 자체가 새어나간다). 정말 예외인 줄은 `# portability-ok: <사유>` 로 표시한다 — 사유는 필수이고, 콜론 뒤가 비면 예외로 인정하지 않는다. 판정은 **정규식이 아니라 셸 워드 토크나이저**로 한다: 옵션 문법을 정규식으로 흉내내면 새 철자(`-oP` → `-Pio` → `--perl-regexp` → `--color=always -P` → `-n --perl-regexp`)가 계속 새어 나가고, 토크나이저는 따옴표 인식이 딸려 와 **인용 문자열이 증거로 계수되는 것**(`printf 'md5'` 를 BSD 대안으로, `printf '# portability-ok: …'` 를 예외로 읽는 것)까지 같이 막는다. 명령 위치도 구분하므로 `command -v timeout` 의 `timeout` 은 인자이지 호출이 아니고, `case` 패턴의 `iteration_cap|timeout|…)` 도 실행이 아니다. 회귀 케이스는 `scripts/check-shell-portability.test.mjs` 30건이며 pre-commit + CI 양쪽에서 돈다 — 가드 자체가 detector 라 커버리지 없이 두면 "볼 수 없었다" 를 "문제 없다" 로 보고한다. 스캔 대상은 `*.sh`/`*.bash`/`*.md` 의 fenced bash 블록에 더해 **shebang 이 sh/bash 인 확장자 없는 tracked 파일**까지다 (`.githooks/pre-commit` 이 그것이고, 이 가드를 실행하는 파일 자신이 사각지대였다). `.llmwiki/`·`.claude/spec/`·`code_review.md`·`AGENTS.md` 는 그 구문을 *설명*할 뿐이라 스캔에서 제외된다.

macOS CI 레그(`validate-codex.yml` 의 `macos` job)는 BSD 폴백이 실제로 실행되는 유일한 지점이다. `env -i PATH=/usr/bin:/bin` 는 쓰지 않는다 — macOS 에서 `jq` 가 Homebrew 경로에 있어 스위트가 도구 부재로 죽는다. 대신 `sed`/`date`/`stat` 이 `--version` 을 거부하는지(=BSD 빌드인지) assert 하고, Homebrew coreutils 가 시스템 도구를 가리면 시끄럽게 실패시킨다. 스위트는 `bash` 가 아니라 `/bin/bash` 로 돌려 bash 3.2 를 강제한다 (러너 PATH 의 Homebrew bash 5 로 돌면 bash-4 전용 구문이 여기서 통과하고 사용자에게서 깨진다).

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,7 @@ shared-source 배선은 6개 가드가 매 PR 과 매 커밋(`.githooks/pre-comm
- `sync-hermes-manifests.mjs --check` — Hermes 어댑터 drift + orphan.
- `check-doc-consistency.mjs` — 플러그인 트리·표·카운트(총 25 / Codex-eligible 23 / Hermes 7)가 `manifest-eligibility.mjs` SoT 와 일치.
- `check-skill-tool-portability.mjs --check` — 공유 스킬 본문의 `AskUserQuestion` 사용이 파일럿 표준 매핑 또는 baseline 에 등록됐는지(미등록 크로스런타임 상호작용 경로 차단).
- `check-shell-portability.mjs` — GNU 전용 셸 구문(`md5sum`·`sed -i`·`grep -P`·`date -d`·`stat -c`·`timeout`·`${VAR,,}`·`mapfile`·`declare -A` 등)이 **폴백도 capability probe 도 없이** 쓰인 경우 차단. 정상 폴백 쌍(`stat -c … || stat -f …`)probe 분기는 통과하고, 증거는 코드만 인정합니다(대체재를 언급하는 주석은 폴백이 아님). 예외는 `# portability-ok: <사유>`.
- `check-shell-portability.mjs` — GNU 전용 셸 구문(`md5sum`·`sed -i`·`grep -P`·`date -d`·`stat -c`·`timeout`·`${VAR,,}`·`mapfile`·`declare -A` 등)이 **폴백도 capability probe 도 없이** 쓰인 경우 차단. 정상 폴백 쌍(`stat -c … || stat -f …`)은 통과하되, `||` 는 **우변에 BSD 대응물이 있을 때만** 폴백으로 봅니다 (`md5sum "$f" || exit 1` 은 통과 못 함). probe 도 대응물과 짝일 때만 인정하고, BSD 대응물이 없는 구문(`grep -P`·`timeout`·bash 4 문법)은 명시적 예외만 받습니다. GNU 긴 옵션(`--perl-regexp`·`--date`·`--format`·`--in-place`)도 동일하게 잡습니다. 증거는 코드만 인정합니다(대체재를 언급하는 주석은 폴백이 아님). 예외는 `# portability-ok: <사유>` — 사유 필수. 판정은 정규식이 아니라 셸 워드 토크나이저로 하므로 옵션 철자(`-Pio`·`--perl-regexp`·`--color=always -P`·`-n --perl-regexp`)를 모두 잡고, 인용 문자열은 증거로 세지 않으며(`printf 'md5'` 는 BSD 대안이 아님), `command -v timeout` 처럼 이름만 언급한 자리와 `case` 패턴은 호출로 보지 않습니다. 스캔 대상에 shebang 이 sh/bash 인 확장자 없는 파일(`.githooks/pre-commit`)도 포함됩니다. 회귀 케이스 30건(`check-shell-portability.test.mjs`)이 pre-commit + CI 에서 함께 돕니다.
- `check-skill-prose.mjs` — 500줄 초과·깊은 참조 경로에 대한 정보성 경고(비차단, 항상 exit 0).

가드와 별개로 두 개의 픽스처 스위트가 같은 자리에서 돕니다 — `plugins/github-dev/skills/cr-fix/tests/run-tests.sh` (1차 경로가 실패한 뒤에만 실행되는 CLI 폴백·CR 상태 경로)와 `plugins/council/skills/convene/tests/run-tests.sh` (스킬 본문에만 존재해 아무도 실행하지 않는 codex/agy 호출 계약 + 레지스트리 TTL 산술). 후자가 지키는 것은 이식성 가드가 볼 수 없는 종류입니다 — agy 호출에서 `< /dev/null` 이 빠지면 그 의석이 영원히 멈추는데, 그건 GNU 전용 구문이 아니라서 `check-shell-portability` 의 관심사가 아닙니다.
Expand Down
Loading
Loading