@kaydash9999/banker-plugins 0.20.2 → 0.21.0
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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +203 -1
- package/PRIVACY.md +34 -4
- package/README.md +46 -15
- package/bin/banker.js +228 -51
- package/bin/lib/codex-hooks.mjs +249 -0
- package/bin/lib/telemetry-config.mjs +33 -16
- package/codex/manifest.json +14 -11
- package/codex/skills/graceful-pause/SKILL.md +30 -0
- package/codex/skills/progress/SKILL.md +42 -0
- package/codex/skills/progress/scripts/progress-plan.mjs +722 -0
- package/codex/skills/progress/scripts/progress.mjs +295 -0
- package/codex/transform-matrix.md +19 -14
- package/commands/setup.md +23 -0
- package/hooks/obsidize-hook.mjs +116 -4
- package/hooks/progress-plan.mjs +1 -1
- package/hooks/progress.mjs +143 -38
- package/hooks/register.mjs +13 -3
- package/hooks/telemetry-count-codex.mjs +222 -0
- package/hooks/update-checkin.mjs +15 -7
- package/hooks/update-fetch.mjs +15 -9
- package/hooks/update-notify.mjs +30 -17
- package/package.json +1 -1
- package/skills/3d-intro-build/SKILL.md +11 -5
- package/skills/3d-intro-build/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-build/references/curate.mjs +48 -14
- package/skills/3d-intro-build/references/preview-lib.mjs +51 -15
- package/skills/3d-intro-build/references/prompts.md +4 -4
- package/skills/3d-intro-build/references/serve.mjs +6 -1
- package/skills/3d-intro-build/references/verify-intro.mjs +16 -9
- package/skills/3d-intro-build/references/video-pool.mjs +250 -135
- package/skills/3d-intro-setup/SKILL.md +41 -10
- package/skills/3d-intro-setup/references/azure-adapter.mjs +374 -81
- package/skills/3d-intro-setup/references/video-pool.mjs +250 -135
- package/skills/anti-drm/SKILL.md +6 -3
- package/skills/anti-drm/scripts/anti-drm.mjs +49 -12
- package/skills/anti-drm/scripts/instruction-toggle.mjs +105 -20
- package/skills/append_wiki/SKILL.md +34 -8
- package/skills/audit-mock/SKILL.md +1 -1
- package/skills/audit-security/SKILL.md +64 -2
- package/skills/compact-wiki/SKILL.md +37 -6
- package/skills/deep-research/SKILL.md +11 -0
- package/skills/docs-setup/SKILL.md +37 -7
- package/skills/lfm/SKILL.md +148 -0
- package/skills/lineage/SKILL.md +25 -15
- package/skills/lineage/lineage.py +564 -301
- package/skills/motion-graphic-make/SKILL.md +1 -1
- package/skills/motion-graphic-make/references/curate.mjs +48 -14
- package/skills/motion-graphic-make/references/preview-lib.mjs +51 -15
- package/skills/motion-graphic-make/references/serve.mjs +6 -1
- package/skills/motion-graphic-setup/SKILL.md +58 -13
- package/skills/obsidizer/SKILL.md +30 -11
- package/skills/obsidizer/obsidize.mjs +50 -12
- package/skills/omc-patch/SKILL.md +86 -23
- package/skills/omc-patch/scripts/omc-patch.mjs +460 -116
- package/skills/payload-mon/SKILL.md +6 -2
- package/skills/payload-mon/scripts/payload-mon.mjs +88 -12
- package/skills/payload-mon/scripts/payload-size.mjs +4 -1
- package/skills/pdf-vision-extract/SKILL.md +11 -6
- package/skills/ralph-qa/SKILL.md +125 -151
- package/skills/ralph-qa/references/gemini-seat.mjs +67 -19
- package/skills/ralph-qa/references/payload-scan.mjs +22 -7
- package/skills/ralph-qa/references/seat-payload.mjs +646 -0
- package/skills/ralph-qa/references/seat-run.mjs +956 -0
- package/skills/ralph-qa/references/seat-spawn.mjs +205 -0
- package/skills/ralph-qa/references/verifier-probe.mjs +57 -16
- package/skills/rfp-author/SKILL.md +5 -3
- package/skills/rfp-author/references/vendor-rfp-structure.md +1 -1
- package/skills/setup-bypass-permissions/SKILL.md +27 -9
- package/skills/setup-bypass-permissions/scripts/codex-bypass.mjs +135 -0
- package/skills/setup-bypass-permissions/scripts/codex-config.mjs +210 -0
- package/skills/setup-harness-factory/SKILL.md +17 -1
- package/skills/setup-insane-search/SKILL.md +16 -0
- package/skills/setup-java/SKILL.md +20 -1
- package/skills/setup-lsp/SKILL.md +56 -13
- package/skills/setup-mcp/SKILL.md +73 -4
- package/skills/setup-node/SKILL.md +31 -6
- package/skills/setup-omc/SKILL.md +24 -0
- package/skills/setup-omc-hud/SKILL.md +42 -9
- package/skills/setup-omc-hud/assets/apply-hud.mjs +242 -27
- package/skills/setup-omc-hud/assets/codex-config.mjs +210 -0
- package/skills/setup-omc-hud/assets/codex-status-line.mjs +90 -0
- package/skills/setup-omc-hud/assets/omc-hud-custom.mjs +81 -14
- package/skills/setup-omc-hud/scripts/claude-update-last.mjs +5 -3
- package/skills/setup-playwright/SKILL.md +45 -2
- package/skills/setup-pwsh/SKILL.md +19 -0
- package/skills/setup-python/SKILL.md +45 -7
- package/skills/setup-sandbox/SKILL.md +30 -5
- package/skills/setup-stitch/SKILL.md +22 -0
- package/skills/setup-tmux/SKILL.md +24 -4
- package/skills/smart-compact/SKILL.md +58 -12
- package/skills/smart-compact/scripts/codex-gate.mjs +384 -0
- package/skills/tone-compact/SKILL.md +2 -1
- package/skills/tone-compact/scripts/instruction-toggle.mjs +105 -20
- package/skills/ultra-chain/SKILL.md +12 -7
- package/skills/ultra-chain/references/preview-lib.mjs +51 -15
- package/skills/ultra-chain/references/serve.mjs +6 -1
- package/skills/ultra-interview/SKILL.md +14 -2
- package/skills/update-banker/SKILL.md +19 -3
- package/skills/vertical-pptx/SKILL.md +6 -3
- package/skills/vertical-pptx-setup/SKILL.md +76 -20
|
@@ -10,11 +10,11 @@ description: "컨텍스트 사용률이 임계(기본 50%)를 넘으면 `append_
|
|
|
10
10
|
→ `/compact` → paste. `smart-compact` 로 무장, `smart-compact --cancel` 로 해제한다.
|
|
11
11
|
`ready-compact` 는 0.14.0 부터 마지막 메시지를 resume 프롬프트만으로 내므로, 예전처럼 `compact-copy` 를 거치지 않는다.
|
|
12
12
|
|
|
13
|
-
## ⚠️ 정직한 실현 한계 (Claude Code 실측)
|
|
13
|
+
## ⚠️ 정직한 실현 한계 (Claude Code, Codex 실측)
|
|
14
14
|
"컨텍스트 50%에서 완전 무인 자동"은 **부분만 가능**하다 — 근거:
|
|
15
|
-
- **감지**: 컨텍스트
|
|
16
|
-
|
|
17
|
-
**정확히 50% 감지 가능**.
|
|
15
|
+
- **감지**: Claude Code 는 컨텍스트 사용률을 **statusLine stdin** 의 `context_window.used_percentage` 로만 실시간
|
|
16
|
+
노출한다(hook은 이 값을 못 받음; auto-compact은 ~95%에서만, 50% 이벤트 없음). → statusLine으로
|
|
17
|
+
**정확히 50% 감지 가능**. Codex 는 훅이 받는 세션 기록에서 계산한다(아래 Codex 절).
|
|
18
18
|
- **자동 실행 가능(어시스턴트)**: `append-wiki` · `ready-compact` — 스킬/도구
|
|
19
19
|
작업이라 어시스턴트가 수행.
|
|
20
20
|
- **자동 불가(TUI 전용)**: `/copy` · `/compact` · **paste** 는 인터랙티브 TUI 동작이라 hook·스킬이
|
|
@@ -26,7 +26,7 @@ description: "컨텍스트 사용률이 임계(기본 50%)를 넘으면 `append_
|
|
|
26
26
|
때만·클립보드 보장 없이 취약). smart-compact은 **가장 신뢰 가능한 최대 자동화**를 택한다.
|
|
27
27
|
|
|
28
28
|
## 메커니즘 (무장 시 설치)
|
|
29
|
-
런타임 인식 — Claude Code
|
|
29
|
+
런타임 인식 — Claude Code 는 statusLine 래퍼, Codex 는 세션 기록을 읽는 훅으로 둘 다 임계를 정확히 잰다.
|
|
30
30
|
|
|
31
31
|
**Claude Code (OMC):**
|
|
32
32
|
1. **statusLine 래퍼(compose-safe)**: 기존 `statusLine.command`(예: OMC HUD)를 **보존**하고, 그것을
|
|
@@ -39,9 +39,54 @@ description: "컨텍스트 사용률이 임계(기본 50%)를 넘으면 `append_
|
|
|
39
39
|
3. **게이트 동작(어시스턴트)**: 주입을 받으면 자동파트를 실행한다. TUI 3단 안내는 `ready-compact` **전에** 한 줄로 하고, 마지막 메시지는 `ready-compact` 의 프롬프트로 끝낸다(안내를 뒤에 붙이면 `/copy` 가 안내문을 복사한다).
|
|
40
40
|
|
|
41
41
|
**Codex (OMX):**
|
|
42
|
-
- Codex
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
- Codex 는 훅 입력에 사용률 칸을 주지 않는다. 대신 모든 훅 입력의 `transcript_path`(세션 기록, rollout)에 요청마다
|
|
43
|
+
`token_count` 가 남는다. 마지막 `last_token_usage.total_tokens` 를 `model_context_window` 로 나눈 값이 사용률이다
|
|
44
|
+
(Codex 0.144.5 실측). Codex 화면의 `context-remaining` 과는 몇 %p 다를 수 있다.
|
|
45
|
+
- 이 스킬 폴더의 `scripts/codex-gate.mjs` 가 계산과 훅을 맡는다. statusLine 래퍼는 쓰지 않는다(Codex 상태표시줄은
|
|
46
|
+
OMX 가 `config.toml` 의 `[tui]` `status_line` 으로 관리한다). 이 SKILL.md 가 있는 디렉터리의 절대경로로 실행한다.
|
|
47
|
+
```bash
|
|
48
|
+
node "<이 스킬 디렉터리 절대경로>/scripts/codex-gate.mjs" on --threshold <N> # 무장(N 은 1~95 정수, 기본 50)
|
|
49
|
+
node "<이 스킬 디렉터리 절대경로>/scripts/codex-gate.mjs" off # 해제
|
|
50
|
+
node "<이 스킬 디렉터리 절대경로>/scripts/codex-gate.mjs" status
|
|
51
|
+
node "<이 스킬 디렉터리 절대경로>/scripts/codex-gate.mjs" usage <rollout 경로> # 수동 확인
|
|
52
|
+
```
|
|
53
|
+
- 파일은 모두 `$CODEX_HOME`(없으면 `~/.codex`)에 둔다.
|
|
54
|
+
|
|
55
|
+
| 파일 | 내용 |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `hooks-custom/banker-smart-compact-gate.mjs` | 훅 사본. 훅은 이 사본을 돌린다 |
|
|
58
|
+
| `hooks.json` | `UserPromptSubmit` 맨 뒤에 훅 묶음 하나 |
|
|
59
|
+
| `smart-compact.json` | 임계값 |
|
|
60
|
+
| `smart-compact-state.json` | 이미 알린 세션 |
|
|
61
|
+
| `hooks.json.smart-compact.bak` | `on`, `off` 가 `hooks.json` 을 바꿀 때마다 그 직전 내용 |
|
|
62
|
+
|
|
63
|
+
- 사본을 두는 까닭: Codex 는 신뢰한 훅을 샌드박스 밖에서 돌린다. 스킬 폴더가 작업 폴더 안에 있으면(프로젝트 범위 설치)
|
|
64
|
+
샌드박스 안의 세션이 그 스크립트를 고쳐 샌드박스 밖에서 돌릴 수 있다. 사본은 샌드박스가 쓸 수 없는 곳에 있다.
|
|
65
|
+
- 같은 까닭으로 `on` 은 임시 폴더의 node 를 거부하고, Codex 안에서 실행될 때는 작업 폴더 안의 node 나 스크립트도 거부한다.
|
|
66
|
+
Codex 를 홈 폴더에서 열었으면 거부된다(그 세션은 홈 전체를 쓸 수 있다). Codex 밖 터미널에서 실행한다.
|
|
67
|
+
PowerShell 명령은 작은따옴표(`'` 와 U+2018~U+201B)를 두 번 써서 인용한다.
|
|
68
|
+
- 맨 뒤에 더하는 까닭: Codex 는 훅 신뢰를 묶음 번호와 함께 기록한다. 앞에 끼우면 OMX 훅의 번호가 밀려 다시 신뢰해야 한다.
|
|
69
|
+
임계값을 명령이 아니라 파일에 두는 것도 같은 까닭이다(명령이 바뀌면 다시 신뢰해야 한다).
|
|
70
|
+
- Windows 의 Codex 는 훅 명령을 PowerShell 로 돌린다(Codex 0.160.1 실측). 그래서 명령은 `& '<node>' '<사본>' hook` 꼴이다.
|
|
71
|
+
- Codex 는 신뢰하지 않은 새 훅을 돌리지 않는다. `on` 뒤 Codex 를 다시 열면 "Hooks need review" 창이 뜬다.
|
|
72
|
+
거기서 Review hooks 나 Trust all 을 고르거나, 나중에 `/hooks` 에서 이 훅을 신뢰하라고 사용자에게 알린다.
|
|
73
|
+
"Continue without trusting" 을 고르면 무장되지 않은 것과 같다.
|
|
74
|
+
- 다시 `on` 하면 임계값만 바꾼다. node 경로가 바뀌었거나 banker 를 갱신해 훅 명령이나 사본이 낡았으면, 같은 자리에서 명령과
|
|
75
|
+
사본을 고친다. 그때는 이 훅만 다시 신뢰하면 된다. `status` 가 "이 판과 다름" 이면 `on` 을 다시 실행한다.
|
|
76
|
+
- `off` 는 이 훅 항목과 사본, 설정, 세션 상태만 지운다. 같은 묶음의 다른 훅은 남긴다. 이 묶음 뒤에 다른 묶음이 있었으면
|
|
77
|
+
그 훅들의 번호가 당겨져 다시 신뢰해야 할 수 있다고 알린다.
|
|
78
|
+
- 훅은 사용률이 임계 이상이면 세션마다 한 번 `additionalContext` 로 체인 실행을 지시한다. `/compact` 로 내려갔다가
|
|
79
|
+
다시 넘으면 또 알린다. 오류가 나도 프롬프트를 막지 않는다. 세션 상태 기록에는 잠금이 없어, 두 세션이 같은 순간에
|
|
80
|
+
프롬프트를 보내면 한 세션이 한 번 더 알림을 받을 수 있다.
|
|
81
|
+
- `on`, `off` 는 `~/.codex` 에 쓴다. Codex 의 `workspace-write` 샌드박스가 막으므로 그 명령을 샌드박스 밖 실행으로
|
|
82
|
+
승인을 요청해 돌린다. 승인을 요청할 수 없거나(승인 정책 `never`) 거부되면, 같은 명령을 Codex 입력창에서 `!` 를 붙여 사용자가 직접 실행하게 안내한다(예: `!node "<경로>" on --threshold <N>`). 사용자가 친 `!` 명령은 샌드박스 밖에서 돈다(Codex 0.144.5, `-a never` 에서 실측). `codex exec` 처럼 사람이 없는 실행이면 그 명령을 보고하고 멈춘다.
|
|
83
|
+
`--add-dir ~/.codex` 로 설정 폴더를 통째로 여는 방법은 쓰지 않는다. 그 세션이 훅과 신뢰 기록을
|
|
84
|
+
고칠 수 있게 되어 샌드박스가 무력해진다. `status`, `usage` 는 읽기만 한다.
|
|
85
|
+
- 훅 묶음이 사라졌으면(다른 도구나 손 편집) `status` 가 "훅이 없음" 으로 알린다. `on` 을 다시 실행한다.
|
|
86
|
+
- banker 나 OMX 를 지우기 전에 `off` 를 실행한다. banker 를 지워도 사본과 훅은 남아 계속 돈다. OMX 의 훅 제거는
|
|
87
|
+
뒤에 다른 묶음이 있으면 번호가 밀린다며 멈출 수 있다.
|
|
88
|
+
- 훅을 쓸 수 없으면(훅 기능 꺼짐, 신뢰 거부) **휴리스틱 폴백**: 어시스턴트가 스스로 컨텍스트 압박을 감지해 체인을
|
|
89
|
+
제안하고, 유저가 트리거. 이때는 50% 정밀 트리거를 보장하지 않는다고 알린다. 준비 스킬은 banker 것이 양 런타임 동일(아래).
|
|
45
90
|
|
|
46
91
|
## 게이트 체인
|
|
47
92
|
무장 후 임계 초과가 감지되면 순서대로:
|
|
@@ -56,8 +101,8 @@ description: "컨텍스트 사용률이 임계(기본 50%)를 넘으면 `append_
|
|
|
56
101
|
Codex=`$banker-append_wiki`·`$banker-ready-compact`; `/copy`·compaction은 양쪽 TUI 내장.)
|
|
57
102
|
|
|
58
103
|
## 활성화 (idempotent · compose-safe)
|
|
59
|
-
`smart-compact [--threshold N]` (기본 N=50)
|
|
60
|
-
1. `~/.claude/settings.json`
|
|
104
|
+
`smart-compact [--threshold N]` (기본 N=50). 아래는 Claude Code 절차이고, Codex 는 위 Codex 절의 `codex-gate.mjs on` 으로 대신한다:
|
|
105
|
+
1. `~/.claude/settings.json` 의 기존 `statusLine`·hook 설정을 **읽어 백업**
|
|
61
106
|
(`~/.claude/.smart-compact-prev.json`).
|
|
62
107
|
2. statusLine 래퍼 스크립트를 설치하고 `statusLine.command` 를 래퍼로 교체(래퍼가 이전 command를 위임 호출).
|
|
63
108
|
3. `UserPromptSubmit` hook 항목을 **추가**(기존 hook 보존, 중복 방지 — 이미 있으면 skip).
|
|
@@ -67,7 +112,7 @@ Codex=`$banker-append_wiki`·`$banker-ready-compact`; `/copy`·compaction은 양
|
|
|
67
112
|
않고 **감싸거나 추가만** 한다(uid는 `id -u` 로 계산, 하드코딩 금지).
|
|
68
113
|
|
|
69
114
|
## 해제
|
|
70
|
-
`smart-compact --cancel
|
|
115
|
+
`smart-compact --cancel` (Codex 는 `codex-gate.mjs off`):
|
|
71
116
|
1. 백업(`~/.claude/.smart-compact-prev.json`)에서 원래 `statusLine`·hook 설정 **복원**.
|
|
72
117
|
2. 래퍼 스크립트·신호 파일·백업 제거.
|
|
73
118
|
3. 무장 해제를 보고. (백업이 없으면 smart-compact가 추가한 항목만 안전 제거.)
|
|
@@ -77,6 +122,7 @@ Codex=`$banker-append_wiki`·`$banker-ready-compact`; `/copy`·compaction은 양
|
|
|
77
122
|
- **완전 자동 과대약속 금지** — `/copy`·`/compact`·paste는 TUI 전용, 유저 핸드오프임을 항상 명시.
|
|
78
123
|
- **uid 하드코딩 금지** — 신호/경로는 `id -u` 로.
|
|
79
124
|
- **hook은 명령을 못 부른다** — `additionalContext` 로 어시스턴트에게 지시할 뿐, 스스로 체인을 실행하지 못함.
|
|
80
|
-
- **Codex
|
|
125
|
+
- **Codex 훅은 신뢰해야 돈다** — 설치 뒤 "Hooks need review" 창이나 `/hooks` 에서 신뢰하라고 반드시 알린다.
|
|
126
|
+
- **Codex 훅 묶음은 맨 뒤에** — 앞에 끼우면 OMX 훅의 신뢰가 풀린다. `hooks.json` 을 손으로 고치지 말고 `codex-gate.mjs` 를 쓴다.
|
|
81
127
|
|
|
82
128
|
ARGUMENTS: [--threshold N] | --cancel (없으면 기본 임계 50%로 무장)
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* smart-compact for Codex: a UserPromptSubmit hook that tells the session when its context passes a threshold.
|
|
4
|
+
*
|
|
5
|
+
* node codex-gate.mjs on [--threshold N] arm: save N (default 50), copy the hook, add it to hooks.json
|
|
6
|
+
* node codex-gate.mjs off disarm: take the hook out, with its copy, threshold and state
|
|
7
|
+
* node codex-gate.mjs status say whether it is armed and the hook is in place and current
|
|
8
|
+
* node codex-gate.mjs usage <rollout> print the context use the session record shows
|
|
9
|
+
* node codex-gate.mjs hook the hook itself (Codex runs it; reads the hook input on stdin)
|
|
10
|
+
*
|
|
11
|
+
* Codex gives a hook no context percentage, but every hook input carries transcript_path, the session record
|
|
12
|
+
* (rollout). Its token_count events hold last_token_usage and model_context_window, so the use of the last
|
|
13
|
+
* request over the window is the context use (measured on Codex 0.144.5).
|
|
14
|
+
* Files live in <CODEX_HOME> (default ~/.codex): smart-compact.json (threshold), smart-compact-state.json
|
|
15
|
+
* (sessions told), hooks.json.smart-compact.bak (hooks.json before each change) and the hook itself, copied to
|
|
16
|
+
* hooks-custom/banker-smart-compact-gate.mjs. Codex runs a trusted hook outside the sandbox, so the hook runs from
|
|
17
|
+
* that copy, where a sandboxed session cannot write, and not from a skill folder that may sit in a workspace.
|
|
18
|
+
* Codex trusts a hook by its handler and records the trust under the group's index: the threshold lives in a file
|
|
19
|
+
* so the command never changes with it, and the group is appended after the ones there, so OMX's hooks keep
|
|
20
|
+
* their indexes. On Windows Codex runs a hook command through PowerShell (measured on Codex 0.160.1), so the
|
|
21
|
+
* command there is `& '<node>' '<script>' hook`.
|
|
22
|
+
* The hook speaks once per session while the use stays at or over the threshold, and again after the use has
|
|
23
|
+
* dropped below it (after /compact). It never blocks a prompt: any error leaves it silent with exit 0.
|
|
24
|
+
* Exit status 1 means nothing was changed; 2 means an unexpected error stopped the command.
|
|
25
|
+
*/
|
|
26
|
+
import {
|
|
27
|
+
chmodSync, closeSync, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, realpathSync,
|
|
28
|
+
renameSync, rmSync, statSync, writeFileSync,
|
|
29
|
+
} from "node:fs";
|
|
30
|
+
import { homedir, tmpdir } from "node:os";
|
|
31
|
+
import { join, posix, win32 } from "node:path";
|
|
32
|
+
import { fileURLToPath } from "node:url";
|
|
33
|
+
|
|
34
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
35
|
+
const COPY_NAME = "banker-smart-compact-gate.mjs";
|
|
36
|
+
const KEEP_SESSIONS = 50;
|
|
37
|
+
const MAX_READ = 32 << 20;
|
|
38
|
+
|
|
39
|
+
export function paths(env = process.env) {
|
|
40
|
+
const home = env.CODEX_HOME || join(homedir(), ".codex");
|
|
41
|
+
return {
|
|
42
|
+
home,
|
|
43
|
+
hooks: join(home, "hooks.json"),
|
|
44
|
+
config: join(home, "smart-compact.json"),
|
|
45
|
+
state: join(home, "smart-compact-state.json"),
|
|
46
|
+
backup: join(home, "hooks.json.smart-compact.bak"),
|
|
47
|
+
copyDir: join(home, "hooks-custom"),
|
|
48
|
+
copy: join(home, "hooks-custom", COPY_NAME),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const isObject = (v) => Boolean(v) && typeof v === "object" && !Array.isArray(v);
|
|
53
|
+
|
|
54
|
+
// JSON from a regular file only: a FIFO or a device would stall the hook. null when missing, odd or unreadable.
|
|
55
|
+
function readJson(file) {
|
|
56
|
+
try {
|
|
57
|
+
return lstatSync(file).isFile() ? JSON.parse(readFileSync(file, "utf8")) : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A new file beside the target, given the target's mode when there is one, then renamed over it. The temp name is
|
|
64
|
+
// created exclusively and a link in the target's place is replaced, not followed. The temp file goes on failure.
|
|
65
|
+
function writeAtomic(file, data) {
|
|
66
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
67
|
+
try {
|
|
68
|
+
writeFileSync(tmp, data, { flag: "wx" });
|
|
69
|
+
const mode = modeOf(file);
|
|
70
|
+
if (mode !== null) chmodSync(tmp, mode);
|
|
71
|
+
renameSync(tmp, file);
|
|
72
|
+
} catch (e) {
|
|
73
|
+
rmSync(tmp, { force: true });
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function modeOf(file) {
|
|
79
|
+
try {
|
|
80
|
+
const st = lstatSync(file);
|
|
81
|
+
return st.isFile() ? st.mode & 0o7777 : null;
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const writeJson = (file, value) => writeAtomic(file, `${JSON.stringify(value, null, 2)}\n`);
|
|
88
|
+
|
|
89
|
+
// The last token_count with usage in a rollout, read from the end in growing spans, so a long session costs one
|
|
90
|
+
// tail read in the usual case; null past `max` bytes, for a file that is not regular (checked before opening, since
|
|
91
|
+
// opening a FIFO waits for a writer), or when none has usage yet (info is null on the first events of a session).
|
|
92
|
+
export function lastUsage(file, max = MAX_READ) {
|
|
93
|
+
if (!statSync(file).isFile()) return null;
|
|
94
|
+
const fd = openSync(file, "r");
|
|
95
|
+
try {
|
|
96
|
+
const size = fstatSync(fd).size;
|
|
97
|
+
for (let span = 1 << 18; ; span *= 4) {
|
|
98
|
+
const start = Math.max(0, size - Math.min(span, max));
|
|
99
|
+
const buf = Buffer.alloc(size - start);
|
|
100
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
101
|
+
const found = scanUsage(buf.toString("utf8"), start > 0);
|
|
102
|
+
if (found || start === 0 || span >= max) return found;
|
|
103
|
+
}
|
|
104
|
+
} finally {
|
|
105
|
+
closeSync(fd);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function scanUsage(text, cutFirst) {
|
|
110
|
+
const lines = text.split("\n");
|
|
111
|
+
if (cutFirst) lines.shift();
|
|
112
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
113
|
+
const found = lines[i].includes('"token_count"') ? usageOf(lines[i]) : null;
|
|
114
|
+
if (found) return found;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function usageOf(line) {
|
|
120
|
+
let rec;
|
|
121
|
+
try {
|
|
122
|
+
rec = JSON.parse(line);
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const info = rec && rec.payload && rec.payload.type === "token_count" ? rec.payload.info : null;
|
|
127
|
+
return info ? usageIn(info) : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function usageIn(info) {
|
|
131
|
+
const used = info.last_token_usage ? info.last_token_usage.total_tokens : NaN;
|
|
132
|
+
const win = info.model_context_window;
|
|
133
|
+
return Number.isFinite(used) && Number.isFinite(win) && win > 0 ? { used, window: win } : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export const percent = (u) => Math.round((u.used * 100) / u.window);
|
|
137
|
+
|
|
138
|
+
export function nudge(pct, threshold) {
|
|
139
|
+
return `smart-compact: 컨텍스트 사용률 ${pct}% (임계 ${threshold}%). 이번 응답에서 위생 체인을 실행한다: `
|
|
140
|
+
+ "$banker-append_wiki, 그다음 한 줄 안내(다음 메시지가 resume 프롬프트이고 그 뒤 /copy, /compact, 붙여 넣기 순서), "
|
|
141
|
+
+ "마지막으로 $banker-ready-compact. /copy, /compact, 붙여 넣기는 사용자가 한다.";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const text = (o, key) => (o && typeof o[key] === "string" ? o[key] : "");
|
|
145
|
+
|
|
146
|
+
// The hook's decision for one prompt: the text to inject, or "" for none. Mutates and returns the state.
|
|
147
|
+
export function decide(input, config, state) {
|
|
148
|
+
const sid = text(input, "session_id");
|
|
149
|
+
const file = text(input, "transcript_path");
|
|
150
|
+
const usage = config && sid && file ? lastUsage(file) : null;
|
|
151
|
+
if (!usage) return { text: "", state };
|
|
152
|
+
const pct = percent(usage);
|
|
153
|
+
if (pct < config.threshold) {
|
|
154
|
+
delete state[sid];
|
|
155
|
+
return { text: "", state };
|
|
156
|
+
}
|
|
157
|
+
if (state[sid]) return { text: "", state };
|
|
158
|
+
state[sid] = Date.now();
|
|
159
|
+
prune(state);
|
|
160
|
+
return { text: nudge(pct, config.threshold), state };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function prune(state) {
|
|
164
|
+
const ids = Object.keys(state).sort((a, b) => state[a] - state[b]);
|
|
165
|
+
for (const old of ids.slice(0, Math.max(0, ids.length - KEEP_SESSIONS))) delete state[old];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function validConfig(c) {
|
|
169
|
+
return isObject(c) && Number.isInteger(c.threshold) && c.threshold >= 1 && c.threshold <= 95 ? c : null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function runHook(p) {
|
|
173
|
+
try {
|
|
174
|
+
process.stdin.setEncoding("utf8");
|
|
175
|
+
let raw = "";
|
|
176
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
177
|
+
const read = readJson(p.state);
|
|
178
|
+
const before = isObject(read) ? read : {};
|
|
179
|
+
const was = JSON.stringify(before);
|
|
180
|
+
const { text: say, state } = decide(JSON.parse(raw), validConfig(readJson(p.config)), before);
|
|
181
|
+
if (JSON.stringify(state) !== was) writeJson(p.state, state);
|
|
182
|
+
if (say) {
|
|
183
|
+
process.stdout.write(JSON.stringify({
|
|
184
|
+
hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: say },
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
/* a hook never blocks the prompt */
|
|
189
|
+
}
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// The command text, or null for a path the platform shell would still read into. POSIX: both paths in double
|
|
194
|
+
// quotes, refusing $, `, " and backslashes. Windows: PowerShell's call operator with single-quoted paths, every
|
|
195
|
+
// character PowerShell takes for a single quote (' and U+2018-U+201B) doubled.
|
|
196
|
+
export function formatCommand(node, script, win = process.platform === "win32") {
|
|
197
|
+
if (/[\r\n\0]/.test(node + script)) return null;
|
|
198
|
+
if (win) {
|
|
199
|
+
const q = (s) => s.replace(/['\u2018-\u201B]/g, "$&$&");
|
|
200
|
+
return `& '${q(node)}' '${q(script)}' hook`;
|
|
201
|
+
}
|
|
202
|
+
return /["$`\\]/.test(node + script) ? null : `"${node}" "${script}" hook`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// The command `on` writes, or null when a location makes it unsafe. Codex runs a trusted hook outside the sandbox,
|
|
206
|
+
// so a node in the temp folder is refused, and when `on` itself runs inside Codex (CODEX_THREAD_ID), a node or a
|
|
207
|
+
// source script inside the workspace is refused too: the sandboxed session could change it first. Paths are judged
|
|
208
|
+
// by the target platform's rules.
|
|
209
|
+
export function hookCommand(node, script, opts = {}) {
|
|
210
|
+
const { win = process.platform === "win32", cwd = process.cwd(), codex = Boolean(process.env.CODEX_THREAD_ID) } = opts;
|
|
211
|
+
const P = win ? win32 : posix;
|
|
212
|
+
const under = (f, dir) => {
|
|
213
|
+
const r = P.relative(dir, f);
|
|
214
|
+
return r === "" || (!r.startsWith("..") && !P.isAbsolute(r));
|
|
215
|
+
};
|
|
216
|
+
if (under(node, tmpdir()) || (codex && [node, SELF].some((f) => under(f, cwd)))) return null;
|
|
217
|
+
return formatCommand(node, script, win);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const COPY_RE = new RegExp(`[/\\\\]${COPY_NAME.replace(/\./g, "\\.")}["']? hook$`);
|
|
221
|
+
const isOurs = (h) => isObject(h) && typeof h.command === "string" && COPY_RE.test(h.command);
|
|
222
|
+
|
|
223
|
+
// hooks.json as an object, or why it cannot be changed. A missing file reads as empty; a link, a file that is not
|
|
224
|
+
// JSON, or an error other than "not found" is refused, so OMX's hooks are never overwritten by mistake.
|
|
225
|
+
function loadHooks(file) {
|
|
226
|
+
const kind = fileKind(file);
|
|
227
|
+
if (kind === "missing") return { doc: { hooks: {} }, groups: [] };
|
|
228
|
+
if (kind) return { why: kind };
|
|
229
|
+
const doc = readJson(file);
|
|
230
|
+
if (!isObject(doc)) return { why: `${file} 이 JSON 객체가 아님` };
|
|
231
|
+
if (doc.hooks === undefined) doc.hooks = {};
|
|
232
|
+
if (!isObject(doc.hooks)) return { why: `${file} 의 hooks 가 객체가 아님` };
|
|
233
|
+
const ups = doc.hooks.UserPromptSubmit;
|
|
234
|
+
if (ups !== undefined && !Array.isArray(ups)) return { why: `${file} 의 UserPromptSubmit 이 배열이 아님` };
|
|
235
|
+
return { doc, groups: ups || [] };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// "" for a regular file, "missing", or why the file cannot be used.
|
|
239
|
+
function fileKind(file) {
|
|
240
|
+
try {
|
|
241
|
+
const st = lstatSync(file);
|
|
242
|
+
return st.isSymbolicLink() || !st.isFile() ? `${file} 이 일반 파일이 아님(링크나 폴더)` : "";
|
|
243
|
+
} catch (e) {
|
|
244
|
+
return e && e.code === "ENOENT" ? "missing" : `${file} 을 확인하지 못함(${e && e.code})`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Where our handler sits: [group index, handler index], or null.
|
|
249
|
+
function findOurs(groups) {
|
|
250
|
+
for (let g = 0; g < groups.length; g++) {
|
|
251
|
+
const hs = isObject(groups[g]) && Array.isArray(groups[g].hooks) ? groups[g].hooks : [];
|
|
252
|
+
const h = hs.findIndex(isOurs);
|
|
253
|
+
if (h >= 0) return [g, h];
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function saveHooks(p, doc) {
|
|
259
|
+
if (existsSync(p.hooks)) writeAtomic(p.backup, readFileSync(p.hooks));
|
|
260
|
+
writeJson(p.hooks, doc);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// The hook copy in <CODEX_HOME>/hooks-custom, refreshed when this version differs. A folder or copy that is a
|
|
264
|
+
// link is refused.
|
|
265
|
+
function installCopy(p) {
|
|
266
|
+
mkdirSync(p.copyDir, { recursive: true });
|
|
267
|
+
if (lstatSync(p.copyDir).isSymbolicLink()) return `${p.copyDir} 이 링크임`;
|
|
268
|
+
if (existsSync(p.copy) && !lstatSync(p.copy).isFile()) return `${p.copy} 이 일반 파일이 아님`;
|
|
269
|
+
const mine = readFileSync(SELF);
|
|
270
|
+
if (!existsSync(p.copy) || !readFileSync(p.copy).equals(mine)) writeAtomic(p.copy, mine);
|
|
271
|
+
return "";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const TRUST = "Codex 를 다시 열면 뜨는 Hooks need review 창이나 /hooks 에서 이 훅을 신뢰하십시오. 신뢰 전에는 돌지 않습니다";
|
|
275
|
+
|
|
276
|
+
function placeHook(p, doc, groups, command) {
|
|
277
|
+
const at = findOurs(groups);
|
|
278
|
+
if (at && groups[at[0]].hooks[at[1]].command === command) return "훅은 이미 있음";
|
|
279
|
+
if (at) groups[at[0]].hooks[at[1]] = { ...groups[at[0]].hooks[at[1]], command, timeout: 5 };
|
|
280
|
+
else groups.push({ hooks: [{ type: "command", command, timeout: 5 }] });
|
|
281
|
+
doc.hooks.UserPromptSubmit = groups;
|
|
282
|
+
saveHooks(p, doc);
|
|
283
|
+
return at ? `훅 명령을 고쳤습니다. ${TRUST}` : `훅을 더했습니다. ${TRUST}`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function turnOn(p, threshold, node = process.execPath) {
|
|
287
|
+
if (!Number.isInteger(threshold) || threshold < 1 || threshold > 95) return { code: 1, msg: "임계값은 1~95 정수" };
|
|
288
|
+
const { doc, groups, why } = loadHooks(p.hooks);
|
|
289
|
+
if (why) return { code: 1, msg: `바꾸지 않음: ${why}` };
|
|
290
|
+
const command = hookCommand(node, p.copy);
|
|
291
|
+
if (!command) return { code: 1, msg: "바꾸지 않음: node 나 훅 경로가 안전하지 않음(셸이 읽는 글자, 임시 폴더의 node, Codex 작업 폴더 안의 node 나 스크립트). Codex 밖 터미널에서 실행하십시오" };
|
|
292
|
+
const bad = installCopy(p);
|
|
293
|
+
if (bad) return { code: 1, msg: `바꾸지 않음: ${bad}` };
|
|
294
|
+
const done = placeHook(p, doc, groups, command);
|
|
295
|
+
writeJson(p.config, { threshold });
|
|
296
|
+
return { code: 0, msg: `켰습니다(임계 ${threshold}%). ${done}` };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Only our handler goes; a group it leaves empty goes too. Groups after a removed one move up an index, and
|
|
300
|
+
// Codex then asks to trust their hooks again, so the message says so.
|
|
301
|
+
export function turnOff(p) {
|
|
302
|
+
const { doc, groups, why } = loadHooks(p.hooks);
|
|
303
|
+
if (why) return { code: 1, msg: `바꾸지 않음: ${why}` };
|
|
304
|
+
const at = findOurs(groups);
|
|
305
|
+
let note = "켜져 있지 않았습니다(훅 없음). 설정만 지웠습니다";
|
|
306
|
+
if (at) {
|
|
307
|
+
const hs = groups[at[0]].hooks.filter((h) => !isOurs(h));
|
|
308
|
+
const shifts = hs.length === 0 && at[0] < groups.length - 1;
|
|
309
|
+
if (hs.length) groups[at[0]].hooks = hs;
|
|
310
|
+
else groups.splice(at[0], 1);
|
|
311
|
+
if (groups.length) doc.hooks.UserPromptSubmit = groups;
|
|
312
|
+
else delete doc.hooks.UserPromptSubmit;
|
|
313
|
+
saveHooks(p, doc);
|
|
314
|
+
note = shifts ? "껐습니다. 뒤에 있던 UserPromptSubmit 훅은 /hooks 에서 다시 신뢰해야 할 수 있습니다" : "껐습니다";
|
|
315
|
+
}
|
|
316
|
+
for (const f of [p.config, p.state, p.copy]) rmSync(f, { force: true });
|
|
317
|
+
return { code: 0, msg: note };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function staleness(p, groups, node) {
|
|
321
|
+
const at = findOurs(groups);
|
|
322
|
+
if (!at) return "no-hook";
|
|
323
|
+
if (groups[at[0]].hooks[at[1]].command !== formatCommand(node, p.copy)) return "stale";
|
|
324
|
+
try {
|
|
325
|
+
return readFileSync(p.copy).equals(readFileSync(SELF)) ? "" : "stale";
|
|
326
|
+
} catch {
|
|
327
|
+
return "stale";
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export function status(p, node = process.execPath) {
|
|
332
|
+
const config = validConfig(readJson(p.config));
|
|
333
|
+
const { groups, why } = loadHooks(p.hooks);
|
|
334
|
+
if (why) return { code: 0, msg: `${config ? "켜짐 표시는 있으나 " : ""}hooks.json 을 읽지 못함: ${why}` };
|
|
335
|
+
const state = staleness(p, groups, node);
|
|
336
|
+
if (!config) return { code: 0, msg: state === "no-hook" ? "꺼짐" : "꺼짐(설정 없음, 훅만 남음). off 로 정리" };
|
|
337
|
+
if (state === "no-hook") return { code: 0, msg: "켜짐 표시는 있으나 훅이 없음(다른 도구나 손 편집이 지웠을 수 있음). on 을 다시 실행" };
|
|
338
|
+
if (state === "stale") return { code: 0, msg: "켜짐. 다만 훅 명령이나 사본이 이 판과 다름(node 경로가 바뀌었거나 banker 를 갱신함). on 을 다시 실행" };
|
|
339
|
+
return { code: 0, msg: `켜짐(임계 ${config.threshold}%). 알림이 없으면 /hooks 에서 이 훅을 신뢰했는지 확인` };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// on's arguments: --threshold N or --threshold=N, nothing else.
|
|
343
|
+
function parseOn(args) {
|
|
344
|
+
if (args.length === 0) return 50;
|
|
345
|
+
const one = args.length === 1 && /^--threshold=(\d+)$/.exec(args[0]);
|
|
346
|
+
if (one) return Number(one[1]);
|
|
347
|
+
return args.length === 2 && args[0] === "--threshold" && /^\d+$/.test(args[1]) ? Number(args[1]) : NaN;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function usageLine(file) {
|
|
351
|
+
const u = file ? lastUsage(file) : null;
|
|
352
|
+
return u ? { code: 0, msg: `${percent(u)}% (${u.used} / ${u.window} 토큰)` } : { code: 1, msg: "token_count 기록 없음" };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const COMMANDS = {
|
|
356
|
+
on: (p, rest) => turnOn(p, parseOn(rest)),
|
|
357
|
+
off: (p, rest) => (rest.length ? null : turnOff(p)),
|
|
358
|
+
status: (p, rest) => (rest.length ? null : status(p)),
|
|
359
|
+
usage: (p, rest) => (rest.length === 1 ? usageLine(rest[0]) : null),
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
async function main(argv) {
|
|
363
|
+
const [cmd = "status", ...rest] = argv;
|
|
364
|
+
const p = paths();
|
|
365
|
+
if (cmd === "hook") return runHook(p);
|
|
366
|
+
const run = Object.prototype.hasOwnProperty.call(COMMANDS, cmd) ? COMMANDS[cmd] : null;
|
|
367
|
+
const out = (run && run(p, rest)) || { code: 1, msg: "사용법: codex-gate.mjs on [--threshold N] | off | status | usage <rollout>" };
|
|
368
|
+
(out.code ? process.stderr : process.stdout).write(`smart-compact: ${out.msg}\n`);
|
|
369
|
+
return out.code;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const invoked = (() => {
|
|
373
|
+
try {
|
|
374
|
+
return Boolean(process.argv[1]) && realpathSync(process.argv[1]) === realpathSync(SELF);
|
|
375
|
+
} catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
})();
|
|
379
|
+
if (invoked) {
|
|
380
|
+
main(process.argv.slice(2)).then((c) => { process.exitCode = c; }, (e) => {
|
|
381
|
+
process.stderr.write(`smart-compact: 예기치 못한 오류: ${e && e.message}\n`);
|
|
382
|
+
process.exitCode = 2;
|
|
383
|
+
});
|
|
384
|
+
}
|
|
@@ -207,7 +207,8 @@ argument-hint: "on | off | status"
|
|
|
207
207
|
| 끊어진 링크라 그대로 두었습니다 | AGENTS.md 가 가리키는 파일이 없음. 링크를 지우거나 바꾸지 않음. 링크와 대상을 보이고 어떻게 할지 물은 뒤 다시 실행 |
|
|
208
208
|
| AGENTS.md 가 끊어진 링크라 on 이 블록을 옮기지 못함 | AGENTS.override.md 에 블록만 남았고 AGENTS.md 가 가리키는 파일이 없음. 링크를 지우거나 바꾸지 않음. 링크와 대상을 보이고 어떻게 할지 물은 뒤 `on` 실행 |
|
|
209
209
|
| 쓰는 사이에 같은 이름이 생겨 그대로 두었습니다 | 다른 프로그램이 그 사이에 같은 이름을 만들었음(링크일 수 있음). 지우거나 바꾸지 않음. 무엇인지 보이고(링크면 `ls -l` 로 링크와 대상 경로만 보이고 대상 내용은 열지 않음) 어떻게 할지 물은 뒤 다시 실행 |
|
|
210
|
-
| 일반 파일이 아님 | 지침 파일 자리(AGENTS.md, AGENTS.override.md)의 폴더나 특수
|
|
210
|
+
| 일반 파일이 아님 | 지침 파일 자리(AGENTS.md, AGENTS.override.md)의 폴더나 특수 파일, 그 백업 자리(`<파일>.tone-compact.bak`)의 폴더는 지우지 않음. 무엇인지 사용자에게 보이고, 옮기거나 이름을 바꿀지 물은 뒤 다시 실행 |
|
|
211
|
+
| 32MiB 보다 큰 파일 | 지침 파일(AGENTS.md, AGENTS.override.md)이 32MiB 를 넘음. 읽지도 바꾸지도 않음. 그 파일이 무엇인지 보이고, 줄일지 옮길지 물은 뒤 다시 실행 |
|
|
211
212
|
| 다른 banker 지침 작업이 끝나지 않아 그대로 두었습니다 | 다른 banker 스킬의 `on`, `off` 가 도는 중. 잠시 뒤 다시 실행. 끝나지 않은 작업이 남긴 잠금은 1분 뒤 저절로 넘어감. 1분이 지나도 같거나 잠금 자리가 폴더면 잠금 경로를 보이고 어떻게 할지 물은 뒤 다시 실행. 잠금을 임의로 지우지 않음 |
|
|
212
213
|
| 네트워크 공유가 잠금 자리에 계속 ESTALE(오래된 파일 핸들)을 돌려줘 그대로 두었습니다 | Codex 폴더가 NFS 같은 네트워크 공유에 있고 공유가 파일 핸들을 잃음. 잠시 뒤 다시 실행. 계속되면 공유 연결 상태를 확인하도록 알림. 잠금을 임의로 지우지 않음 |
|
|
213
214
|
| 쓸 권한이 없음 | 해당 파일과 폴더 권한 확인 |
|