@kaydash9999/banker-plugins 0.19.0 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "metadata": {
8
8
  "description": "Claude Code skills + commands — QA, audit, docs, architecture, dependency setup, and wiki; the banker plugin marketplace.",
9
- "version": "0.19.0"
9
+ "version": "0.19.2"
10
10
  },
11
11
  "plugins": [
12
12
  {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "banker",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "description": "Claude Code plugin: QA/audit/docs/architecture skills + commands, a /banker:setup installer for OS dependencies (playwright, etc.), and wiki compaction.",
5
5
  "author": {
6
6
  "name": "smallOpenSource",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,68 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.19.2] - 2026-10-16
4
+
5
+ ### Changed
6
+ - `anti-drm` SKILL: 종료 코드 1 행에 `앞서 바꾼 파일:` 줄이 있으면 그 파일은 이미 바뀌었다고 적는다(tone-compact 와 같게).
7
+ - 유지보수자용: SKILL 과 명령 문서의 PowerShell 블록이 Windows PowerShell 5.1 에서 구문 오류 없이 읽히는지 Windows 에서 시험한다. 자리 표시는 괄호 안쪽에 공백이 없는 꼴만 낱말로 바꿔 읽는다(공백 낀 `< in.txt >` 는 5.1 이 거부하는 리디렉션이라 그대로 둠). 알려진 오류 두 줄을 시험이 잡는지 먼저 보고, 파싱하며 코드를 불러오는 블록(`using module`, `using assembly`)은 파싱하지 않고 실패로 알린다. Windows PowerShell 5.1 은 제 경로로 부른다. pwsh 7 은 5.1 보다 넓게 받아 Windows 가 아닌 OS 에서는 건너뛴다.
8
+
9
+ ### Fixed
10
+ - `lineage` 3.0.7: 짝 없는 서로게이트(U+D800~U+DFFF 가 짝 없이 든 JSON 이스케이프)가 든 전사 줄은 그 글자를 U+FFFD 로 바꾸고 줄마다 한 번 경고한 뒤 계속한다. 검토 묶음, 검토 결정, 결정 캐시를 읽을 때와 페이지와 묶음을 쓸 때(제목이나 파일 이름에서 온 것)도 경고 없이 같은 글자로 바꾼다. 예전에는 기본 흐름의 검토 묶음(`--emit-review`)과 `--rulebase` 페이지가 트레이스백으로 멈췄다. uuid 가 글이 아닌 줄(목록, 객체, 숫자, 참과 거짓)은 경고 뒤 줄 번호로 id 를 만든다(빈 글과 없는 uuid 는 예전처럼 경고 없이). 예전에는 검토 묶음이 멈췄다. 역할이 user, assistant 가 아닌 줄은 기록 종류로 읽는다. 예전에는 묶음 적용이 `malformed turns` 로 멈췄다.
11
+ - `lineage`: 검토자가 쓴 결정 id 와 묶음의 턴 id 는 한 줄로 찍히지 않는 글자가 있으면 이스케이프해 보인다. 예전에는 줄바꿈이나 제어 문자가 든 id 가 `[lineage] PASS:` 같은 가짜 줄을 만들 수 있었다.
12
+ - 업데이트 확인 체크인 훅: 사용 기록을 보낸 직후 비운다. 예전에는 바뀐 스킬 목록을 받는 동안(최대 30초) 비우지 않아, 그 사이 시작한 세션이 같은 기록을 다시 보냈다.
13
+ - `tone-compact`, `anti-drm`: 잠금 자리가 ESTALE 을 계속 내면 쉬면서 다시 보고, 기다림이 끝나면 거부한다. 예전에는 기다리는 동안 쉬지 않고 lstat 을 되풀이해 코어 하나를 썼다. ESTALE 이 한 번만 나는 죽은 잠금은 예전처럼 정리한다.
14
+
15
+ ## [0.19.1] - 2026-10-16
16
+
17
+ ### Changed
18
+ - 유지보수자용: lineage 의 `parse_turns` 를 동작 그대로 기록 판정, 내용 조각, 항목 하나 처리로 나눴다(함수 규칙 50줄, 복잡도 10). 특성 시험 6개가 동작을 고정한다.
19
+ - 유지보수자용: lineage 시험(`TestRangeFiltersE2E`)이 만든 임시 폴더를 지운다. 예전에는 한 번 돌릴 때마다 임시 폴더에 6개씩 남았다.
20
+ - 유지보수자용: lineage 시험이 실제 `~/.cache/lineage` 에 요약 캐시를 남기지 않는다(모듈을 시작할 때 캐시를 임시 폴더로 돌림). 요약 캐시의 0600 권한을 시험이 고정한다.
21
+ - 유지보수자용: gen-skill-changes, obsidize 훅, 업데이트 확인 훅에서 나눈 main 을 특성 시험으로 고정한다. smoke 와 시험의 npm 오프라인 설정은 철자만 다른 같은 변수에 지지 않는다(Windows 는 변수 이름의 대소문자를 가리지 않아 대문자 변수 하나로도 졌다). smoke 의 파이썬 단계도 임시 HOME 에서 돈다. `bin/banker.test.mjs` 는 상속한 `BANKER_NO_TELEMETRY`, `BANKER_NO_UPDATE_CHECK`, `BANKER_TELEMETRY_ENDPOINT` 를 지운다.
22
+ - `omc-patch`: 적용할 항목과 이 도구로 지울 수 없는 옛 URL 칸이 함께 있으면 결과 줄은 `적용되지 않은 항목이 있습니다.` 이고, 그 칸은 결과 줄 아래 `참고:` 두 줄로 알린다. 예전에는 막힘 줄이 결과가 되어 스킬이 훅 패치까지 하지 않았다. 막힘 결과 줄과 참고 줄은 `옛 URL 칸이 이 도구로 지울 수 없는 곳에 있습니다.` 로 바꿨다(하드 링크처럼 이 이름에서는 지우고 다른 이름에만 남는 때도 맞게). 시도 전 판단은 `지울 수 없음`, 시도해 실패한 것은 `지우지 못함` 으로 문구를 나눴다.
23
+ - `omc-patch` SKILL: 지울 수 없는 옛 기록은 2단계 승인 질문에서 지울지 남길지 함께 묻는다. 남기면 그 줄 하나만 예상된 실패로 보고 검증까지 간다. 기록 설명을 도구 동작에 맞췄다(링크 너머에 그 칸이 없으면 알리지 않음, 되돌리기는 링크가 아닌 기록이면 JSON 이 아니거나 읽지 못해도 그냥 지움).
24
+ - 유지보수자용: omc-patch 시험이 특수 파일인 고정 기록(FIFO, 링크 고리, 끊어진 링크, 폴더, 소켓), origin 복구 실패 뒤의 기록 삭제, FIFO 기록의 되돌리기를 덮는다.
25
+ - `anti-drm` SKILL: 설정 파일이 올바르지 않을 때의 절차를 `on` 표와 문제 해결 표에서 하나로 맞췄다. 다시 정할지 묻고, 그 파일은 새 파일로 바뀌며(그 자리가 폴더나 특수 파일이거나, 64KiB 보다 크거나, 읽을 수 없는 파일이면 바꾸지 않고 거부. 링크와 하드 링크는 그 이름만 바뀌고 `.bak` 에 남지 않음) anti-drm 형식이 아니면 `anti-drm.json.bak` 에 한 번 남는다. 에이전트는 그 파일을 직접 지우거나 옮기지 않는다.
26
+ - `tone-compact`, `anti-drm` SKILL: 문제 해결 표에서 파일을 보일 때는 `ls -l` 로 종류와 링크 대상 경로를 먼저 보이고, 링크가 아닌 일반 파일만 내용을 연다. 링크일 수 있는 행(쓰는 사이에 생긴 이름, 같은 이름의 다른 규칙 파일)에도 적었다. 심어 둔 링크가 가리키는 파일(키 등)이 세션에 오르지 않게 한다.
27
+ - `tone-compact`, `anti-drm` SKILL: `.bak` 파일은 내용을 열지 않고 경로만 알린다. 링크가 가리키던 파일(키 등)의 사본일 수 있다.
28
+ - 유지보수자용: anti-drm 시험이 비차단 표준 입력의 `EAGAIN` 재시도를 덮는다.
29
+ - 유지보수자용: tone-compact 의 링크 고리 시험과 소켓 시험을 나눴다. 소켓을 만들 수 없는 곳(경로가 100바이트 이상, listen 실패)에서는 소켓 시험이 통과가 아니라 건너뜀으로 보인다.
30
+ - 유지보수자용: /progress 시험이 도구 호출, 계획 읽기, 턴 끝과 시작, 할 일 목록 뒤의 다시 그리기 요청, 답 없이 끝나는 알림 턴, 진행 중 항목 둘을 덮고, 작업 도구 읽기를 무작위로 겹친 모형(시드 고정)과 대조한다. 시험의 엔진 대역을 함수 규칙(50줄) 안으로 나눴다.
31
+
32
+ ### Fixed
33
+ - `lineage` 3.0.6: 다른 런타임이나 손으로 만든 전사에서 자료형이 틀린 줄(숫자인 type, 문자열인 message, 목록인 도구 이름, 글이 아닌 text 항목), 객체가 아닌 줄, 너무 깊게 중첩된 줄, 파서가 읽지 못하는 긴 숫자가 든 줄은 경고 뒤 건너뛴다. 글이 아닌 timestamp 는 없는 것으로 읽고, 이름이 null 인 도구는 `?` 로 센다. 예전에는 줄 하나 때문에 내보내기 전체가 트레이스백으로 멈췄다. 경고 줄은 전사에서 온 이름을 이스케이프해 제어 글자로 줄을 꾸미지 못한다. 너무 깊게 중첩된 검토 묶음과 표본은 메시지와 함께 거부한다.
34
+ - `lineage` 3.0.6: 요약 캐시와 결정 캐시는 64KiB 이하의 일반 파일만 읽고 링크를 따르지 않는다(Windows 는 열기 전 확인만 한다). 입력 id 열쇠를 쓰는 임시 이름에 놓인 링크도 따라가지 않는다. 캐시 자리의 FIFO 에서 멈추거나, 링크가 가리키는 다른 파일의 내용을 요약으로 싣거나 그 파일을 덮어쓰던 것을 막는다. 요약 쓰기는 결정 캐시처럼 새 임시 파일과 교체로 한다.
35
+ - `lineage` 3.0.6: 검토 모델이 폭 없는 글자만으로 쓴 요약은 규칙 요약으로 바꾼다. 예전에는 페이지의 요약 줄이 빈 줄로 보였다.
36
+ - `lineage` 3.0.6: 글자 방향을 바꾸는 제어 문자 9자(U+202A~202E, U+2066~2069)를 페이지(날짜 범위 포함)에 `[U+202E]` 같은 보이는 표지로 넣는다. 예전에는 그대로 들어가 화면의 글자 순서가 실제와 달라 보일 수 있었다(예: `gpj.exe` 가 `exe.jpg` 로 보임).
37
+ - `lineage` 3.0.6: Windows 에서도 페이지와 캐시를 LF 줄 끝으로 쓴다. 예전에는 같은 전사에서 Windows 만 CRLF 로 써 바이트가 달랐다.
38
+ - 업데이트 확인 훅: 응답 본문은 256KiB 까지만 받고, 요청 하나는 30초 안에 끝낸다(시험용 값으로도 늘지 않는다). 예전에는 조금씩 계속 보내는 서버에서 훅이 끝나지 않아 세션을 시작할 때마다 하나씩 쌓였고, 큰 응답을 모두 메모리에 받았다.
39
+ - 업데이트 확인 훅: 체크인에 싣는 스킬 이름을 사용 기록을 쓰는 훅과 같은 규칙(`banker:<이름>`, 이름은 64자까지)으로 거른다. 사용 기록에 다른 꼴의 줄이 섞여도 보내지 않는다.
40
+ - obsidize 훅: 로그 줄의 제어 글자를 공백으로 접고 사유를 500자에서 자른다. 로그 자리가 링크면 쓰지 않고(저장소가 실어 올 수 있음. `O_NOFOLLOW` 가 없는 Windows 는 열기 전에 본다), FIFO 여도 멈추지 않는다. 새로 만드는 로그 파일은 이 계정만 읽고 쓴다(0600).
41
+ - `omc-patch`: 문법 오류로 되돌리는 쓰기까지 실패하면 스택 트레이스 대신 `롤백도 실패` 와 백업 위치를 알리고 실패로 센다. 도구가 처리하지 못한 오류는 경로를 정화한 한 줄과 종료 1 로 끝난다.
42
+ - `omc-patch`: 읽을 수 없는 훅(권한, 폴더, FIFO)은 `읽지 못함` 으로 이름을 대고 실패로 센다. 예전에는 FIFO 로 가는 링크에서 멈췄고 나머지는 스택 트레이스였다. 쓸 권한이 없는 훅과 FIFO 같은 특수 파일을 가리키는 훅은 새 파일로 바꿔 쓰지 않는다(`쓰지 않음`, `되돌리지 않음`, `--check` 가 미리 알림). Windows 는 전부터 거부했고 POSIX 가 말없이 바꾸던 것을 맞췄다. hooks.json 이 폴더나 FIFO 이거나 읽을 수 없으면 `--check` 는 `hooks.json 을 읽지 못함`, 적용은 `hooks.json 을 건드리지 않음` 으로 이름을 대고 센다(예전에는 FIFO 에서 멈추고 폴더는 스택 트레이스였다).
43
+ - `omc-patch`: 같은 폴더에서 둘이 동시에 돌아도 거짓 `변환 실패` 와 `되돌리지 않음` 을 내지 않는다. 다른 실행이 백업을 먼저 가져간 경우는 되돌린 것으로 보고, 복구는 됐는데 백업만 못 지우면 그렇게 알린다.
44
+ - `omc-patch`: 파일을 쓸 때 임시 파일을 디스크까지 내린 뒤 rename 한다(상류 OMC 와 같은 꼴).
45
+ - `omc-patch`: 고정 해제에 실패하면 원래 URL 을 호스트까지만 낸다(사용자 정보, 경로, 쿼리, 조각에는 비밀이 들 수 있음. 호스트 모양이 아니면 그것도 내지 않음). `--check` 의 미고정 줄과 git 오류 줄의 쿼리 값은 이름과 상관없이 가린다. git 이 오류 문구 없이 끝나면 명령 줄(URL 전체가 듦) 대신 어떻게 끝났는지만 낸다. 판이 여럿일 때 hooks.json 의 `[!]` 줄에 판 이름을 붙이고, 비활성 판의 읽기 실패는 결과 줄 밑에 참고로 알린다. 손 복구 안내는 링크 대상이 이 도구 계열로 패치된 파일일 때만 덮어쓰라고 한다. 백업 없음 안내는 임시 tar 파일을 거친다(Windows 의 tar 는 파이프를 받지 않음). installed_plugins.json 을 읽지 못하면 `읽지 못함` 이라 하고 파일 글을 내지 않는다.
46
+ - `omc-patch`: 인자 없는 apply 는 대상 판이 캐시에 있어도 활성이 아니면 전환한다. 예전에는 첫 전환이 실패한 PC 에서 다시 돌리면 판을 못 바꾸고도 성공으로 보였다.
47
+ - `omc-patch` SKILL: 5단계의 고정 확인 명령은 origin 이 `omc-pinned://` 로 시작하는지만 보고 `pinned` 나 `not-pinned` 를 낸다. 예전 명령은 쿼리의 토큰을 그대로 보였다.
48
+ - `omc-patch`: 고정 기록(`PINNED`)이 링크면 첫 고정, 되돌린 뒤 다시 적용, 옛 URL 칸 지우기 모두 링크를 따라가 쓰지 않는다. 확인한 뒤 링크로 바뀐 자리도 같다. 예전에는 첫 고정과 다시 적용이 링크가 가리키는 파일을 덮었다.
49
+ - `omc-patch`: 옛 URL 칸이 든 기록에 하드 링크로 이어진 다른 이름이 있으면 적용(첫 고정이 새 기록으로 덮을 때 포함)과 되돌리기는 `이 이름에서만 지움` 으로 알리고 실패로 센다. `--check` 는 `이 이름에서만 지울 수 있음` 으로 미리 알린다(origin 이 아직 고정되지 않은 PC 에서는 적용이 처음 알린다). 다른 이름을 찾을 inode(Windows 는 파일 ID) 번호도 낸다. 예전에는 이 이름만 지우고 성공으로 끝나 다른 이름에 URL 이 남았다.
50
+ - `omc-patch`: git 이 마켓플레이스 설정을 읽지 못하면(git 없음, 다른 계정 저장소라 거부, 깨진 설정) `origin 을 읽지 못함` 으로 알리고 실패로 센다. `--check` 의 결과 줄은 `마켓플레이스 origin 을 확인하지 못했습니다.` 다. 예전에는 `origin remote 없음 — 이미 fetch 불가` 로 성공했고, 되돌리기는 고정이 그대로인데 `되돌리기 완료` 라고 했다.
51
+ - `omc-patch`: 되돌리기는 마켓플레이스가 없거나, git 저장소가 아니거나, origin 이 없어도 고정 기록을 지운다. 마켓플레이스가 없는 PC 에서도 옛 기록의 URL 칸을 점검하고 지운다.
52
+ - `omc-patch`: 옛 URL 칸은 JSON 최상위 칸으로 판정한다. 값 안에 그 글자가 든 기록이나 이스케이프로 쓴 키를 잘못 보지 않는다.
53
+ - `omc-patch`: 설정 파일(`installed_plugins.json`, `settings.json`), `--root` 폴더의 이름표, `--scan` 에 준 파일, 고정 기록도 일반 파일만 읽는다. 예전에는 그 자리가 FIFO 면 `--check` 까지 멈췄다. 고정 기록과 손 복구 안내가 들여다보는 링크 대상은 확인과 읽기 사이에 FIFO 로 바뀌어도 멈추지 않는다.
54
+ - `omc-patch`: `--check` 는 기록이 든 폴더에 쓸 수 없으면 `(적용하면 지움)` 대신 `지울 수 없음 (쓰지 못함: ...)` 으로 알린다.
55
+ - `tone-compact`, `anti-drm`: Codex 에 새 AGENTS.md 를 만들 때 그 사이에 같은 이름이 생기면 따라가 쓰지 않고 거부한다(종료 1). 심어 둔 링크도 거부한다. 단 Windows 는 아무것도 가리키지 않는 링크를 따라가 그 대상 파일을 만든다(`wx` 의 Windows 동작, 링크를 심으려면 관리자 권한이나 개발자 모드가 필요). 예전에는 링크가 가리키는 파일에 썼다.
56
+ - `tone-compact`, `anti-drm`: 샌드박스가 권한 확인은 통과시키고 쓰기를 막으면 `예기치 못한 오류`(종료 2) 대신 `쓸 권한이 없어 그대로 두었습니다`(종료 1)로 끝난다. 메시지는 시스템이 거부한 자리(백업 파일 등)를 댄다. Windows 에서 백업 자리에 폴더가 있을 때의 EPERM 도 이 거부로 끝난다. 파일 둘을 바꾸던 중에 거부되면 첫 파일은 이미 바뀌었을 수 있고, 메시지 끝의 `앞서 바꾼 파일:` 줄이 그 파일을 댄다(같은 이름이 생겨 거부할 때도 같다).
57
+ - `tone-compact`, `anti-drm`: AGENTS.override.md 에 이 스킬 블록만 있고 AGENTS.md 가 끊어진 링크면 status 가 `on` 이 블록을 옮기지 못한다고 알린다. 예전에는 `on` 으로 옮긴다고 했지만 `on` 은 거부했다.
58
+ - `tone-compact`, `anti-drm`: 규칙 파일 자리가 링크(끊어진 링크, 옛 사본으로 가는 링크)였으면 `on` 이 일반 파일로 바꾼 뒤 결과에 알린다. 링크가 가리키던 곳은 건드리지 않는다. 예전에는 말없이 바꿨다.
59
+ - `anti-drm`: 설정을 저장하는 `config` 와 설정 값을 준 `on` 도 설정 파일 자리가 링크나 하드 링크였으면 알리고, anti-drm 이 쓰지 않은 원래 파일을 `.bak` 에 남겼는지, 먼저 남긴 파일이나 일반 파일이 아닌 것(링크, 폴더)이 그 자리에 있어 남기지 않았는지 알린다. 알림은 `.bak` 에 쓸지 정한 그 확인에서 나온다.
60
+ - `anti-drm`: 설정을 저장할 때 설정 파일 자리가 링크나 하드 링크(이름이 둘 이상인 일반 파일)면 가리키던 파일을 읽거나 `anti-drm.json.bak` 에 복사하지 않는다. 그 이름만 새 파일로 바뀌고 가리키던 파일은 그대로 남는다. 64KiB 를 넘거나 읽을 수 없는 파일을 가리키는 링크와 하드 링크도 이제 거부하지 않고 그 이름만 바꾼다. 저장 전에 기존 파일을 열 때 Linux 와 macOS 는 링크를 따르지 않고(`O_NOFOLLOW`), 이 플래그가 없는 Windows 는 연 뒤 그 이름이 연 파일인지 파일 ID 로 다시 본다. 그 사이에 다른 파일로 바뀌었으면 `확인하는 사이에 바뀐 자리라 설정을 저장하지 않았습니다` 로 거부한다. 예전에는 링크가 가리키는 작은 파일(키 등)의 사본이 `~/.config/banker/` 에 일반 파일로 남아, 일반 파일만 내용을 여는 SKILL 규칙에 걸리지 않았다. 0.19.0 까지 남은 사본은 지우지 않으니, 설정 파일 자리에 링크를 둔 적이 있으면 `anti-drm.json.bak` 을 확인한 뒤 지운다.
61
+ - `tone-compact`, `anti-drm`: 남은 잠금을 정리하다 네트워크 공유가 `ESTALE` 을 내면 이미 사라진 잠금으로 본다. 예전에는 종료 2 로 멈췄다.
62
+ - `/progress`: `/clear` 를 하면 패널이 바로 빈 목록을 그린다. 예전에는 다음 프롬프트까지 옛 단계가 남아 보였다.
63
+ - `/progress`: 계획을 건드린 뒤 검토 문서(약한 계획)를 건드리면, 두 읽기가 거꾸로 끝나도 계획이 그대로다. 세션이 읽기만 한 단계 없는 계획은 체크박스가 있는 검토 문서를 대신한다.
64
+ - `/progress`: 작업 도구 읽기(TaskGet, TaskList)가 겹쳐 돌아도 나중에 시작한 읽기가 이긴다. 작업이 없다는 TaskGet 과 작업을 뺀 TaskList(판에 없던 작업 포함)도 그렇다. 예전에는 늦게 끝난 옛 읽기가 지워진 작업을 되살릴 수 있었다.
65
+
3
66
  ## [0.19.0] - 2026-10-15
4
67
 
5
68
  npm 에는 0.18.1 을 게시하지 않았다. npm 에서는 이 판이 0.18.0 다음이며, 아래 [0.18.1] 의 변경도 함께 담는다.
@@ -48,6 +48,31 @@ export const NPM_LATEST_URL = 'https://registry.npmjs.org/@kaydash9999/banker-pl
48
48
  // 테스트는 BANKER_SKILL_CHANGES_URL 로 loopback mock 을 주입해 실제 네트워크를 때리지 않는다.
49
49
  export const SKILL_CHANGES_URL = 'https://raw.githubusercontent.com/smallOpenSource/banker_plugins/main/skill-changes.json';
50
50
 
51
+ // usage-log 와 체크인 본문에 실리는 스킬/커맨드 이름 꼴. 쓰는 훅 둘과 보내는 훅이 같은 규칙을 쓴다.
52
+ // 이름은 64자까지다(가장 긴 실제 이름은 24자). 모델이 지어낸 긴 이름이 기록과 체크인에 실리지 않게 한다.
53
+ export const SKILL_NAME_RE = /^banker:[a-z0-9_-]{1,64}$/i;
54
+
55
+ // 갱신 확인 요청 하나의 전체 마감(ms). 시험이 줄일 수 있게 BANKER_NET_DEADLINE_MS 를 받되 줄이기만 한다: 30초보다
56
+ // 길면 30초다. setTimeout 이 1ms 로 바꿔 버리는 값(0 이하, 숫자 아님)도 버리고 30초를 쓴다. 문서에 적지 않는 시험용 장치다.
57
+ export function netDeadlineMs(env = process.env) {
58
+ const v = Number(env.BANKER_NET_DEADLINE_MS);
59
+ return Number.isFinite(v) && v > 0 ? Math.min(v, 30_000) : 30_000;
60
+ }
61
+
62
+ // 네트워크 응답 본문을 maxBytes 까지만 모아 done(문자열)로 넘긴다. 한도를 넘으면 연결을 끊고 done(null),
63
+ // 응답 스트림 오류도 done(null). 낯선 서버가 끝없이 보내도 메모리가 한도 밖으로 늘지 않게 한다.
64
+ export function collectBody(res, maxBytes, done) {
65
+ const chunks = [];
66
+ let size = 0;
67
+ res.on('data', (c) => {
68
+ size += c.length;
69
+ if (size > maxBytes) { res.destroy(); done(null); return; }
70
+ chunks.push(c);
71
+ });
72
+ res.on('end', () => done(Buffer.concat(chunks).toString('utf8')));
73
+ res.on('error', () => done(null));
74
+ }
75
+
51
76
  // config.json 을 읽어 객체로 반환. 파일 없음·malformed JSON·권한 오류 등 어떤 예외에도 던지지 않고 {} 반환.
52
77
  export function readConfig() {
53
78
  try {
@@ -17,16 +17,22 @@
17
17
  * - Exits 0 on every path. A hook cannot block or alter a tool result and must not try.
18
18
  * - Every error appends exactly one line to <wikiDir>/.obsidizer-hook.log. Exit 0 hides
19
19
  * stderr too, so that log is the only channel by which a permanently broken hook is
20
- * discoverable rather than silently doing nothing forever.
20
+ * discoverable rather than silently doing nothing forever. Only a regular file there is
21
+ * written: a link (a cloned repository can carry one) or a pipe gets no line.
21
22
  */
22
23
  import { spawnSync } from 'node:child_process';
23
- import { appendFileSync, existsSync, readdirSync, statSync } from 'node:fs';
24
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readdirSync, statSync, writeSync } from 'node:fs';
24
25
  import { basename, dirname, join, resolve } from 'node:path';
25
26
  import { fileURLToPath } from 'node:url';
26
27
 
27
28
  const CANONICALIZER = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'skills', 'obsidizer', 'obsidize.mjs');
28
29
  const FLAG = '.obsidizer';
29
30
  const LOG = '.obsidizer-hook.log';
31
+ const LOG_REASON_MAX = 500;
32
+ // Append, never through a link, and never wait for a pipe's reader. Windows has neither flag: there the log place is
33
+ // looked at first and a link is left alone (one swapped in after that look is still followed there).
34
+ const NO_FOLLOW = constants.O_NOFOLLOW ?? 0;
35
+ const LOG_FLAGS = constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT | NO_FOLLOW | (constants.O_NONBLOCK ?? 0);
30
36
  // OMC owns these three. index.md is rewritten on every ingest, so it is always the freshest
31
37
  // file in the vault — the scan fallback below would target it first without this guard.
32
38
  const RESERVED = new Set(['index.md', 'log.md', 'environment.md']);
@@ -46,14 +52,27 @@ function readStdin() {
46
52
  }
47
53
 
48
54
  function log(wikiDir, tool, reason) {
49
- const line = `${new Date().toISOString()}\t${tool}\t${String(reason).replace(/\s+/g, ' ').trim()}\n`;
55
+ // One line of plain text: whitespace and control characters (ESC among them) fold to a space, and a long reason is cut.
56
+ const text = String(reason).replace(/[\s\x00-\x1f\x7f-\x9f]+/g, ' ').trim();
57
+ const line = `${new Date().toISOString()}\t${tool}\t${text.length > LOG_REASON_MAX ? `${text.slice(0, LOG_REASON_MAX)}...` : text}\n`;
58
+ const at = join(wikiDir, LOG);
50
59
  try {
51
- appendFileSync(join(wikiDir, LOG), line);
60
+ if (!NO_FOLLOW && isLink(at)) return;
61
+ const fd = openSync(at, LOG_FLAGS, 0o600);
62
+ try {
63
+ if (fstatSync(fd).isFile()) writeSync(fd, line);
64
+ } finally {
65
+ closeSync(fd);
66
+ }
52
67
  } catch {
53
68
  // The log is best-effort; the failure path must never throw.
54
69
  }
55
70
  }
56
71
 
72
+ function isLink(path) {
73
+ try { return lstatSync(path).isSymbolicLink(); } catch { return false; }
74
+ }
75
+
57
76
  function isDir(path) {
58
77
  try { return statSync(path).isDirectory(); } catch { return false; }
59
78
  }
@@ -303,9 +303,11 @@ function markOf(s, id) {
303
303
  return s.marks.get(id);
304
304
  }
305
305
 
306
- // Whether a list or a read begun at (since, order) is older than what the pane holds of a task: the task changed
307
- // after it began (a delete too), or a later-begun read already brought it.
306
+ // Whether a list or a read begun at (since, order) is older than what the pane holds of a task: a list begun after it
307
+ // was taken in (a list names the whole list, so this holds for a task it left out that the pane never held, too), the
308
+ // task changed after it began (a delete too), or a later-begun read already brought it.
308
309
  function stale(s, id, since, order) {
310
+ if (order < s.listAt) return true;
309
311
  const m = s.marks.get(id);
310
312
  return Boolean(m) && ((m.changedRev ?? 0) > since || order < (m.seenAt ?? 0));
311
313
  }
@@ -342,8 +344,6 @@ function updateTask(s, e, r, now, { order }) {
342
344
  putTask(s, { id, subject: e.subject, description: e.description, activeForm: e.activeForm, status }, now);
343
345
  }
344
346
 
345
- // Whether the pane changed a task (a delete too) after a list of it began.
346
- const changedAfter = (s, id, since) => (s.marks.get(id)?.changedRev ?? 0) > since;
347
347
  // The status a list or a read begun at order shows for a task: a status update begun after it stands, also for a
348
348
  // task the pane did not hold then.
349
349
  const readStatus = (s, id, order, status) => {
@@ -376,20 +376,28 @@ function listTasks(s, _e, r, now, { since, order }) {
376
376
  const named = new Set(listed.map((t) => String(t.id)));
377
377
  s.tasks = new Map();
378
378
  for (const t of listed) takeListed(s, before, t, { over, since, order, now });
379
- for (const [id, held] of before) if (!named.has(id) && changedAfter(s, id, since)) s.tasks.set(id, held);
379
+ for (const [id, held] of before) {
380
+ if (named.has(id)) continue;
381
+ if (stale(s, id, since, order)) s.tasks.set(id, held);
382
+ else markOf(s, id).seenAt = order;
383
+ }
380
384
  }
381
385
 
382
- // TaskGet reads one task: one it cannot find is gone, and a completed one off the pane stays off. A read older than
383
- // what the pane holds of the task changes nothing.
386
+ // TaskGet reads one task: one it cannot find, or finds deleted, is gone, and a completed one off the pane stays off. A
387
+ // read older than what the pane holds of the task changes nothing. A read taken in makes an earlier-begun read of the
388
+ // task stale, one that leaves the task off the pane too.
384
389
  function getTask(s, e, r, now, { since, order }) {
385
390
  if (stale(s, String(e.taskId), since, order)) return;
386
391
  const t = r?.result?.task;
387
- if (t === null) s.tasks.delete(String(e.taskId));
392
+ if (t === null) {
393
+ s.tasks.delete(String(e.taskId));
394
+ markOf(s, String(e.taskId)).seenAt = order;
395
+ }
388
396
  if (!t) return;
389
397
  const id = String(t.id);
390
398
  const status = readStatus(s, id, order, t.status);
391
- if (status === 'deleted' || (status === 'completed' && !s.tasks.has(id))) return;
392
- putTask(s, { id, subject: t.subject, description: t.description, status }, now);
399
+ if (status === 'deleted') s.tasks.delete(id);
400
+ else if (status !== 'completed' || s.tasks.has(id)) putTask(s, { id, subject: t.subject, description: t.description, status }, now);
393
401
  markOf(s, id).seenAt = order;
394
402
  }
395
403
 
@@ -476,13 +484,17 @@ function current(s, path, { touch, since }) {
476
484
  return path === s.planPath && s.taken <= since;
477
485
  }
478
486
 
487
+ // A file with no steps does not push out a plan that has some, unless the session wrote it itself; a weak plan holds no
488
+ // such place, since any plan replaces it.
489
+ const stepless = (s, path, plan, read) => path !== s.planPath && !plan.total && Boolean(s.plan?.total) && !read.own && planKind(s.planPath) !== 'weak';
490
+
479
491
  // Whether what was read stays off the pane: no plan; a review round or open questions while the pane shows another
480
492
  // plan; or a file with no steps in place of a plan that has some, unless the session wrote it itself (an agent's
481
493
  // review written beside a plan, or a note the session only read, does not push the plan away; a plan the session
482
494
  // writes is the plan, steps or not).
483
495
  const keepsOff = (s, path, plan, read) => !plan
484
496
  || (path !== s.planPath && Boolean(s.plan) && read.weak === true)
485
- || (path !== s.planPath && !plan.total && Boolean(s.plan?.total) && !read.own);
497
+ || stepless(s, path, plan, read);
486
498
 
487
499
  // The ids in a tree, to keep the unfolded ones that are still there when a plan is read again.
488
500
  function treeIds(nodes, out = new Set()) {
@@ -501,7 +513,7 @@ export function takePlan(s, path, text, read = {}) {
501
513
  const plan = parsed(path, text);
502
514
  if (keepsOff(s, path, plan, read)) return;
503
515
  const moving = path !== s.planPath;
504
- if (read.touch !== undefined) s.taken = read.touch;
516
+ if (read.touch !== undefined && !read.weak) s.taken = read.touch; // a weak plan does not make an older touch late
505
517
  if (moving && s.selected === 'plan') s.selected = null;
506
518
  const ids = treeIds(plan.tree);
507
519
  s.planOpen = moving ? new Set() : new Set([...s.planOpen].filter((id) => ids.has(id)));
@@ -97,6 +97,7 @@ export const register = (on) => {
97
97
  on('session.end', async ($, e, next) => {
98
98
  pauseReset();
99
99
  progressReset();
100
+ $.ui.invalidate('ui.render'); // the pane shows at once what /clear emptied
100
101
  return next(e);
101
102
  });
102
103
 
@@ -22,11 +22,7 @@
22
22
  */
23
23
  import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
24
24
 
25
- import { configDir, countingActive, recordUsedSkill, USAGE_LOG_PATH, LOG_MAX_LINES } from '../bin/lib/telemetry-config.mjs';
26
-
27
- // banker:<name> 전체 일치 - 이름 문자열 자체를 검증한다(TAB/개행 등 이질 문자가 섞여
28
- // usage-log 의 `name\thour` 형식을 깨는 것을 막는다). i 플래그는 매칭만 관대하게(실제 이름은 소문자).
29
- const NAME_RE = /^banker:[a-z0-9_-]+$/i;
25
+ import { configDir, countingActive, recordUsedSkill, USAGE_LOG_PATH, LOG_MAX_LINES, SKILL_NAME_RE } from '../bin/lib/telemetry-config.mjs';
30
26
 
31
27
  function readStdin() {
32
28
  return new Promise((resolvePromise, rejectPromise) => {
@@ -39,11 +35,13 @@ function readStdin() {
39
35
  }
40
36
 
41
37
  // PostToolUse(Skill) payload 에서 모델이 자동 호출한 banker:<name> 스킬 이름을 추출한다.
38
+ // 이름은 telemetry-config 의 SKILL_NAME_RE 전체 일치로 검증한다(TAB, 개행 같은 글자가 usage-log 의
39
+ // `name\thour` 형식을 깨지 않게).
42
40
  // 주 소스는 tool_input.skill, 없거나 미매치면 보조로 tool_response.commandName 을 시도한다.
43
41
  function extractName(payload) {
44
42
  const candidates = [payload?.tool_input?.skill, payload?.tool_response?.commandName];
45
43
  for (const c of candidates) {
46
- if (typeof c === 'string' && NAME_RE.test(c)) return c;
44
+ if (typeof c === 'string' && SKILL_NAME_RE.test(c)) return c;
47
45
  }
48
46
  return null;
49
47
  }
@@ -22,11 +22,7 @@
22
22
  */
23
23
  import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
24
24
 
25
- import { configDir, countingActive, recordUsedSkill, USAGE_LOG_PATH, LOG_MAX_LINES } from '../bin/lib/telemetry-config.mjs';
26
-
27
- // banker:<name> 전체 일치 - 이름 문자열 자체를 검증한다(TAB/개행 등 이질 문자가 섞여
28
- // usage-log 의 `name\thour` 형식을 깨는 것을 막는다). i 플래그는 매칭만 관대하게(실제 이름은 소문자).
29
- const NAME_RE = /^banker:[a-z0-9_-]+$/i;
25
+ import { configDir, countingActive, recordUsedSkill, USAGE_LOG_PATH, LOG_MAX_LINES, SKILL_NAME_RE } from '../bin/lib/telemetry-config.mjs';
30
26
 
31
27
  function readStdin() {
32
28
  return new Promise((resolvePromise, rejectPromise) => {
@@ -39,11 +35,13 @@ function readStdin() {
39
35
  }
40
36
 
41
37
  // UserPromptExpansion payload 에서 사용자가 직접 호출한 banker:<name> 이름을 추출한다.
38
+ // 이름은 telemetry-config 의 SKILL_NAME_RE 전체 일치로 검증한다(TAB, 개행 같은 글자가 usage-log 의
39
+ // `name\thour` 형식을 깨지 않게).
42
40
  // command_source 가 'plugin' 이고 command_name 이 banker:<name> 형식일 때만 반환, 그 외 null.
43
41
  function extractName(payload) {
44
42
  if (payload?.command_source !== 'plugin') return null;
45
43
  const name = payload?.command_name;
46
- return (typeof name === 'string' && NAME_RE.test(name)) ? name : null;
44
+ return (typeof name === 'string' && SKILL_NAME_RE.test(name)) ? name : null;
47
45
  }
48
46
 
49
47
  // 파일의 개행 개수 = 줄 수(각 append 가 끝에 개행을 붙이므로). 없거나 읽기 실패면 0.
@@ -27,27 +27,32 @@ import {
27
27
  installedVersion,
28
28
  writeUpdateCache,
29
29
  changedSkillsBetween,
30
+ SKILL_NAME_RE,
31
+ collectBody,
32
+ netDeadlineMs,
30
33
  } from '../bin/lib/telemetry-config.mjs';
31
34
 
32
35
  const NET_TIMEOUT_MS = 10_000;
36
+ const NET_DEADLINE_MS = netDeadlineMs(); // 교환 전체 마감(위 값은 무활동 기준).
37
+ const MAX_BODY_BYTES = 256 * 1024; // 체크인 응답, 매니페스트 모두 수 KB 다.
33
38
 
34
39
  // usage-log 각 줄 `<name>\t<hour 0-23>` 를 스킬별 x 시간대별 count 로 집계한다.
35
40
  // 빈 줄·TAB 없는 줄·이름 없는 줄·hour 가 0-23 정수가 아닌 줄은 무시한다.
36
41
  // 반환 예: { 'banker:compact-copy': { '9': 3, '14': 1 }, 'banker:setup': { '9': 1 } }.
37
42
  function aggregate(raw) {
38
- const counts = {};
43
+ const counts = Object.create(null);
39
44
  for (const line of raw.split('\n')) {
40
45
  if (!line.trim()) continue; // 빈 줄 무시.
41
46
  const tab = line.indexOf('\t');
42
47
  if (tab < 0) continue; // TAB 없는 malformed 줄 무시.
43
48
  const name = line.slice(0, tab).trim();
44
49
  const hourStr = line.slice(tab + 1).trim();
45
- if (!name) continue; // 이름 없는 줄 무시.
50
+ if (!SKILL_NAME_RE.test(name)) continue; // banker:<slug> 꼴이 아닌 줄 무시(쓰는 훅과 같은 규칙).
46
51
  if (!/^\d+$/.test(hourStr)) continue; // 정수 아닌 hour 무시(음수·소수·비수치 배제).
47
52
  const hour = Number(hourStr);
48
53
  if (hour > 23) continue; // 0-23 범위 밖 무시(하한은 \d+ 가 보장).
49
54
  const key = String(hour); // '09' -> '9' 정규화.
50
- if (!counts[name]) counts[name] = {};
55
+ if (!counts[name]) counts[name] = Object.create(null);
51
56
  counts[name][key] = (counts[name][key] || 0) + 1;
52
57
  }
53
58
  return counts;
@@ -65,7 +70,8 @@ function isLoopback(hostname) {
65
70
  function post(url, body) {
66
71
  return new Promise((resolvePromise) => {
67
72
  let done = false;
68
- const finish = (responseBody = null) => { if (!done) { done = true; resolvePromise(responseBody); } };
73
+ let deadline = null;
74
+ const finish = (responseBody = null) => { if (!done) { done = true; clearTimeout(deadline); resolvePromise(responseBody); } };
69
75
  let parsed;
70
76
  try {
71
77
  parsed = new URL(url);
@@ -85,14 +91,10 @@ function post(url, body) {
85
91
  const req = transport.request(parsed, {
86
92
  method: 'POST',
87
93
  headers: { 'content-type': 'application/json', 'content-length': Buffer.byteLength(body) },
88
- }, (res) => {
89
- let data = '';
90
- res.on('data', (c) => { data += c; });
91
- res.on('end', () => finish(data));
92
- res.on('error', () => finish());
93
- });
94
+ }, (res) => collectBody(res, MAX_BODY_BYTES, finish));
94
95
  req.on('error', () => finish()); // 연결 거부/리셋 등은 즉시 여기로.
95
96
  req.setTimeout(NET_TIMEOUT_MS, () => req.destroy());
97
+ deadline = setTimeout(() => { req.destroy(); finish(); }, NET_DEADLINE_MS);
96
98
  req.write(body);
97
99
  req.end();
98
100
  } catch {
@@ -120,7 +122,8 @@ function extractLatest(responseBody) {
120
122
  function fetchSkillChangesMap(url) {
121
123
  return new Promise((resolvePromise) => {
122
124
  let done = false;
123
- const finish = (result) => { if (!done) { done = true; resolvePromise(result); } };
125
+ let deadline = null;
126
+ const finish = (result) => { if (!done) { done = true; clearTimeout(deadline); resolvePromise(result); } };
124
127
  let parsed;
125
128
  try {
126
129
  parsed = new URL(url);
@@ -137,20 +140,18 @@ function fetchSkillChangesMap(url) {
137
140
  const req = transport.request(parsed, { method: 'GET' }, (res) => {
138
141
  const status = res.statusCode || 0;
139
142
  if (status >= 300 && status < 400) { res.resume(); return finish(null); } // 리다이렉트 미추적.
140
- let data = '';
141
- res.on('data', (c) => { data += c; });
142
- res.on('end', () => {
143
+ collectBody(res, MAX_BODY_BYTES, (data) => {
143
144
  try {
144
- const obj = JSON.parse(data);
145
+ const obj = JSON.parse(data); // data null(한도 초과, 스트림 오류)은 null 로 읽혀 아래서 걸러진다.
145
146
  finish((obj && typeof obj === 'object' && !Array.isArray(obj)) ? obj : null);
146
147
  } catch {
147
148
  finish(null);
148
149
  }
149
150
  });
150
- res.on('error', () => finish(null));
151
151
  });
152
152
  req.on('error', () => finish(null));
153
153
  req.setTimeout(NET_TIMEOUT_MS, () => req.destroy());
154
+ deadline = setTimeout(() => { req.destroy(); finish(null); }, NET_DEADLINE_MS);
154
155
  req.end(); // GET - 요청 본문 없음.
155
156
  } catch {
156
157
  finish(null);
@@ -192,8 +193,8 @@ async function main() {
192
193
  const url = endpoint();
193
194
  if (!url) return; // 엔드포인트 없음 - 무동작(로그 미변경).
194
195
  const responseBody = await post(url, checkinBody());
196
+ resetLog(); // 보낸 직후 비운다. 매니페스트를 받는 동안(최대 30초) 시작한 세션이 같은 로그를 다시 보내지 않게.
195
197
  await refreshUpdateCache(responseBody);
196
- resetLog();
197
198
  }
198
199
 
199
200
  main()
@@ -19,9 +19,11 @@ import { readFileSync } from 'node:fs';
19
19
  import http from 'node:http';
20
20
  import https from 'node:https';
21
21
 
22
- import { UPDATE_CHECK_PATH, UPDATE_THROTTLE_MS, NPM_LATEST_URL, writeUpdateCache } from '../bin/lib/telemetry-config.mjs';
22
+ import { UPDATE_CHECK_PATH, UPDATE_THROTTLE_MS, NPM_LATEST_URL, writeUpdateCache, collectBody, netDeadlineMs } from '../bin/lib/telemetry-config.mjs';
23
23
 
24
24
  const NET_TIMEOUT_MS = 10_000;
25
+ const NET_DEADLINE_MS = netDeadlineMs(); // 교환 전체 마감(위 값은 무활동 기준).
26
+ const MAX_BODY_BYTES = 256 * 1024; // npm /latest 응답은 수 KB 다.
25
27
 
26
28
  // loopback 호스트만 평문 http 를 허용한다(로컬 테스트 mock 전용). update-checkin.mjs 와 동일 판별:
27
29
  // URL.hostname 은 IPv6 를 대괄호로 감싸므로([::1]) 벗겨서 비교한다.
@@ -45,7 +47,8 @@ function readCache() {
45
47
  function fetchLatestVersion(url) {
46
48
  return new Promise((resolvePromise) => {
47
49
  let done = false;
48
- const finish = (result) => { if (!done) { done = true; resolvePromise(result); } };
50
+ let deadline = null;
51
+ const finish = (result) => { if (!done) { done = true; clearTimeout(deadline); resolvePromise(result); } };
49
52
  let parsed;
50
53
  try {
51
54
  parsed = new URL(url);
@@ -67,21 +70,19 @@ function fetchLatestVersion(url) {
67
70
  res.resume();
68
71
  return finish(null);
69
72
  }
70
- let data = '';
71
- res.on('data', (c) => { data += c; });
72
- res.on('end', () => {
73
+ collectBody(res, MAX_BODY_BYTES, (data) => {
73
74
  try {
74
- const obj = JSON.parse(data);
75
+ const obj = JSON.parse(data); // data null(한도 초과, 스트림 오류)은 null 로 읽혀 version 이 없다.
75
76
  const v = obj && typeof obj === 'object' ? obj.version : undefined;
76
77
  finish((typeof v === 'string' && v) ? v : null);
77
78
  } catch {
78
79
  finish(null); // 빈/malformed 본문.
79
80
  }
80
81
  });
81
- res.on('error', () => finish(null));
82
82
  });
83
83
  req.on('error', () => finish(null)); // 연결 거부/리셋 등은 즉시 여기로.
84
84
  req.setTimeout(NET_TIMEOUT_MS, () => req.destroy());
85
+ deadline = setTimeout(() => { req.destroy(); finish(null); }, NET_DEADLINE_MS);
85
86
  req.end(); // GET - 요청 본문 없음.
86
87
  } catch {
87
88
  finish(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaydash9999/banker-plugins",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "description": "banker — Claude Code + Codex CLI plugin (QA, audit, docs, architecture, wiki skills). Install via Claude Code marketplace, or `npm i -g @kaydash9999/banker-plugins` then `banker setup`.",
5
5
  "bin": {
6
6
  "banker": "bin/banker.js"
@@ -45,7 +45,7 @@ argument-hint: "on | off | status | config"
45
45
  |---|---|
46
46
  | `없음` | 아래 "질문" 을 한 뒤 "설정 넘기기" 대로 `on --runtime <런타임> --stdin` 실행 |
47
47
  | 파일 경로 | `on --runtime <런타임>` |
48
- | `올바르지 않음` | 이유를 전하고 "질문" 으로 설정을 다시 정한 뒤 `없음` 행과 같은 명령 실행 |
48
+ | `올바르지 않음` | 이유를 전하고 설정을 다시 정할지 물음. 정하면 "질문" 뒤 `없음` 행과 같은 명령 실행. 그 파일은 새 파일로 바뀌고(그 자리가 폴더나 특수 파일이거나, 64KiB 보다 크거나, 읽을 수 없는 파일이면 바꾸지 않고 거부함. 링크와 하드 링크(이름이 둘 이상인 일반 파일)는 거부하지 않고 그 이름만 일반 파일로 바꾸며, 가리키던 파일은 그대로 두고 `.bak` 에 남기지 않음), anti-drm 형식이 아니면 `anti-drm.json.bak` 에 한 번 남음. 에이전트가 그 파일을 직접 지우거나 옮기지 않음 |
49
49
 
50
50
  3. 성공하면 출력의 `적용할 규칙:` 아래 규칙을 이 세션에 바로 적용. 다음 세션부터는 자동 적용
51
51
 
@@ -130,7 +130,7 @@ argument-hint: "on | off | status | config"
130
130
  | 코드 | 뜻 | 처리 |
131
131
  |---|---|---|
132
132
  | 0 | 완료. 이미 그 상태였던 경우 포함 | 결과 전달 |
133
- | 1 | 거부 또는 사용법 오류. 설정 값을 준 `on` 과 `config` 는 설정을 저장한 뒤 갱신하지 못한 런타임이 있어도 1 | 메시지 그대로 전달 |
133
+ | 1 | 거부 또는 사용법 오류. 설정 값을 준 `on` 과 `config` 는 설정을 저장한 뒤 갱신하지 못한 런타임이 있어도 1. 메시지에 `앞서 바꾼 파일:` 이 있으면 그 파일은 이미 바뀜 | 메시지 그대로 전달 |
134
134
  | 2 | 예기치 못한 오류 | 메시지 전달 후 `status` 로 확인 |
135
135
 
136
136
  - 출력이 비어 있으면 실행 실패. 성공으로 보고하지 않음
@@ -213,9 +213,10 @@ argument-hint: "on | off | status | config"
213
213
  - `off` 는 지우지 않음
214
214
  - `.md` 와 `.json` 은 금지할 수 없음. 규칙 파일과 설정 파일의 형식
215
215
  - 목록은 각각 64개까지
216
- - 읽는 파일: 64KiB 이하의 일반 파일만. 소유자는 이 계정, 또는 root 만 쓸 수 있는 root
217
- - 폴더, FIFO, 64KiB 를 넘는 파일, 읽을 수 없는 파일은 덮어쓰지 않음
218
- - anti-drm 이 쓰지 않은 파일이 그 자리에 있으면 `anti-drm.json.bak` 에 한 번 남김 (이 계정만 읽기 가능)
216
+ - 읽는 파일: 64KiB 이하의 일반 파일만(링크면 가리키는 파일을 읽음). 소유자는 이 계정, 또는 root 만 쓸 수 있는 root
217
+ - 폴더, FIFO, 64KiB 를 넘는 파일, 읽을 수 없는 파일은 덮어쓰지 않음(링크와 하드 링크는 다음 줄)
218
+ - 저장할 때 링크와 하드 링크(이름이 둘 이상인 일반 파일)는 그 이름만 새 파일로 바꿈. 가리키던 파일은 읽지 않고 그대로 둠
219
+ - anti-drm 이 쓰지 않은 파일이 그 자리에 있으면 `anti-drm.json.bak` 에 한 번 남김 (이 계정만 읽기 가능). 링크와 하드 링크는 남기지 않음
219
220
  - 설정 폴더
220
221
  - Claude Code: `$CLAUDE_CONFIG_DIR`, 없으면 `~/.claude`
221
222
  - Codex: `$CODEX_HOME`, 없으면 `~/.codex`
@@ -233,22 +234,28 @@ argument-hint: "on | off | status | config"
233
234
 
234
235
  ## 문제 해결
235
236
 
237
+ - 아래 표에서 파일을 보인다는 것은 `ls -l` 로 그 이름의 종류와 링크 대상 경로를 보이는 것이다. 링크가 아닌 일반 파일임을 본 뒤에만 내용을 연다. 심어 둔 링크가 가리키는 파일(키 등)이 세션에 오르지 않게 한다.
238
+ - `.bak` 파일(`anti-drm.json.bak`, `<파일>.anti-drm.bak`)은 내용을 열지 않고 경로만 알린다. 링크가 가리키던 파일(키 등)의 사본일 수 있다. 0.19.0 까지는 설정 파일 자리의 링크도 그 대상을 남겼다.
239
+
236
240
  | 메시지 | 조치 |
237
241
  |---|---|
238
242
  | 설정이 없습니다 | "질문" 으로 설정을 정해 `on` 에 넘김 |
239
243
  | 표준 입력에 설정 줄이 없습니다 | heredoc 이 비었음. "설정 넘기기" 대로 줄을 넣어 다시 실행 |
240
244
  | 표준 입력이 64KiB 보다 큽니다 | "답 처리" 대로 정규화한 값만 넘김 |
241
- | 설정 파일이 올바르지 않습니다 | 이유를 전하고 `config` 로 다시 만들지 물은 뒤 진행. 다른 사용자 소유 파일은 지우거나 옮기지 않음. 무엇인지 보이고 사용자가 정하게 함 |
245
+ | 설정 파일이 올바르지 않습니다 | 이유를 전하고 설정을 다시 정할지 물은 뒤 진행. 정하면 "질문" 뒤 켜는 중이면 `on --runtime <런타임> --stdin`, 설정만 바꿀 때는 `config --stdin` 실행. 그 파일은 새 파일로 바뀌고(그 자리가 폴더나 특수 파일이거나, 64KiB 보다 크거나, 읽을 수 없는 파일이면 바꾸지 않고 거부함. 링크와 하드 링크(이름이 둘 이상인 일반 파일)는 거부하지 않고 그 이름만 일반 파일로 바꾸며, 가리키던 파일은 그대로 두고 `.bak` 에 남기지 않음), anti-drm 형식이 아니면 `anti-drm.json.bak` 에 한 번 남음(먼저 남긴 파일이나 일반 파일이 아닌 것이 그 자리에 있으면 남기지 않고 결과에 알림). 에이전트가 그 파일을 직접 지우거나 옮기지 않음 |
242
246
  | 일반 파일이 아니라 설정을 저장하지 않았습니다 | 설정 경로의 폴더나 특수 파일은 지우지 않음. 무엇인지 보이고, 옮길지 다른 경로(`BANKER_ANTI_DRM_CONFIG`)를 쓸지 물은 뒤 다시 실행 |
243
247
  | 64KiB 보다 큰 파일이라, 읽을 수 없는 파일이라 설정을 저장하지 않았습니다 | 그 파일이 무엇인지 보이고, 권한을 고칠지 옮길지 물은 뒤 다시 실행 |
248
+ | 확인하는 사이에 바뀐 자리라 설정을 저장하지 않았습니다 | 설정 파일을 연 사이에 다른 프로그램이 그 자리를 바꿈(Windows, 링크일 수 있음). 지우거나 바꾸지 않음. 무엇인지 보이고(링크면 `ls -l` 로 링크와 대상 경로만 보이고 대상 내용은 열지 않음) 어떻게 할지 물은 뒤 다시 실행 |
244
249
  | anti-drm 이 쓰는 형식이라 금지할 수 없습니다 | 금지 목록에서 `.md`, `.json` 을 뺌 |
245
- | 같은 이름의 다른 파일이 있음 | `rules/banker-anti-drm.md` 가 banker 파일이 아님. 지우거나 이름을 바꾸지 않음. 내용을 보이고 어떻게 할지 물은 뒤 다시 실행 |
250
+ | 같은 이름의 다른 파일이 있음 | `rules/banker-anti-drm.md` 가 banker 파일이 아님. 지우거나 이름을 바꾸지 않음. 내용을 보이고(링크면 `ls -l` 로 링크와 대상 경로만 보이고 대상 내용은 열지 않음) 어떻게 할지 물은 뒤 다시 실행 |
246
251
  | 표시의 짝이 맞지 않음 | 지침 파일에 anti-drm 표시 줄이 한쪽만 있거나 문장 속에 있음. 또는 블록 안에 다른 표시 줄이 있음. 짝을 새로 맞추지 않음. 남은 표시 줄을 보이고 지울지 물은 뒤 다시 실행 |
247
252
  | 다른 banker 블록만 있어 Codex 가 AGENTS.md 를 읽지 않음 | 메시지가 말한 스킬에서 `on` 실행 |
248
253
  | 설정 폴더가 없음 | 해당 런타임 미설치. 또는 설정 폴더 환경 변수 값 확인 |
249
254
  | 규칙이 지금 설정이나 이 버전과 다름 | `on` 한 번 실행 |
250
255
  | UTF-8 이 아닌 파일 | 지침 파일을 바꾸지 않음. 인코딩을 보이고, 백업을 만든 뒤 UTF-8 로 바꿀지 물은 뒤 다시 실행 |
251
256
  | 끊어진 링크라 그대로 두었습니다 | AGENTS.md 가 가리키는 파일이 없음. 링크를 지우거나 바꾸지 않음. 링크와 대상을 보이고 어떻게 할지 물은 뒤 다시 실행 |
257
+ | AGENTS.md 가 끊어진 링크라 on 이 블록을 옮기지 못함 | AGENTS.override.md 에 블록만 남았고 AGENTS.md 가 가리키는 파일이 없음. 링크를 지우거나 바꾸지 않음. 링크와 대상을 보이고 어떻게 할지 물은 뒤 `on` 실행 |
258
+ | 쓰는 사이에 같은 이름이 생겨 그대로 두었습니다 | 다른 프로그램이 그 사이에 같은 이름을 만들었음(링크일 수 있음). 지우거나 바꾸지 않음. 무엇인지 보이고(링크면 `ls -l` 로 링크와 대상 경로만 보이고 대상 내용은 열지 않음) 어떻게 할지 물은 뒤 다시 실행 |
252
259
  | 일반 파일이 아님 | 지침 파일 자리(AGENTS.md, AGENTS.override.md)의 폴더나 특수 파일은 지우지 않음. 무엇인지 사용자에게 보이고, 옮기거나 이름을 바꿀지 물은 뒤 다시 실행 |
253
260
  | 다른 banker 지침 작업이 끝나지 않아 그대로 두었습니다 | 다른 banker 스킬의 `on`, `off` 가 도는 중. 잠시 뒤 다시 실행. 끝나지 않은 작업이 남긴 잠금은 1분 뒤 저절로 넘어감. 1분이 지나도 같거나 잠금 자리가 폴더면 잠금 경로를 보이고 어떻게 할지 물은 뒤 다시 실행. 잠금을 임의로 지우지 않음 |
254
261
  | 쓸 권한이 없음 | 해당 파일과 폴더 권한 확인 |