deel-local-cli 1.13.1 → 1.14.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/README.ko.md CHANGED
@@ -20,7 +20,7 @@
20
20
 
21
21
  [![Node.js CI](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/test.yml?branch=main&logo=github&logoColor=white&label=Node.js%20CI)](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
22
22
  [![CodeQL](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/codeql.yml?branch=main&logo=github&logoColor=white&label=CodeQL)](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
23
- [![tests](https://img.shields.io/badge/tests-7%2C046%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/ko/develop.md)
23
+ [![tests](https://img.shields.io/badge/tests-7%2C095%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/ko/develop.md)
24
24
 
25
25
  [![dependencies](https://img.shields.io/badge/dependencies-0-1a7f37)](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
26
26
  [![ESM](https://img.shields.io/badge/ESM-Node%2020%2B-5FA04E?logo=javascript&logoColor=white)](package.json)
@@ -94,7 +94,7 @@
94
94
  | [속도와 씀씀이](docs/ko/tuning.md) | 단계별 추론 강도 · 프리픽스 캐시 · 컨텍스트 길이 |
95
95
  | [안전망과 사내 반입](docs/ko/safety.md) | 되돌리기 · 작업 범위 · 감사기록 · 심사 서류 |
96
96
  | [설정](docs/ko/config.md) · [개발](docs/ko/develop.md) | 환경변수 · 실행 옵션 · 검사 돌리기 · 폴더 구조 |
97
- | [릴리스 노트](docs/ko/releases.md) | [1.13.x](docs/ko/releases/1.13.md) · [1.12.x](docs/ko/releases/1.12.md) · [1.10.x](docs/ko/releases/1.10.md) · [1.9.x](docs/ko/releases/1.9.md) · [그 앞](docs/ko/releases.md) |
97
+ | [릴리스 노트](docs/ko/releases.md) | [1.14.x](docs/ko/releases/1.14.md) · [1.13.x](docs/ko/releases/1.13.md) · [1.12.x](docs/ko/releases/1.12.md) · [1.10.x](docs/ko/releases/1.10.md) · [1.9.x](docs/ko/releases/1.9.md) · [그 앞](docs/ko/releases.md) |
98
98
 
99
99
  ---
100
100
 
@@ -267,7 +267,7 @@ deel --offline
267
267
  무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
268
268
 
269
269
  ```
270
- deel 1.13.1 ⌂ 이 안
270
+ deel 1.14.0 ⌂ 이 안
271
271
  보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
272
272
  ```
273
273
 
@@ -292,7 +292,7 @@ deel --offline
292
292
  수집·전송하는 것이 없습니다. 텔레메트리, 사용 통계, 오류 보고 전부 없습니다.
293
293
  대화 기록·되돌리기 이력·설정은 작업 폴더의 `.deel/` 안에만 남습니다.
294
294
 
295
- > 검증: `npm test` 의 network·web·mcp 검사 159항목. 허용되지 않은 서버에 실제로
295
+ > 검증: `npm test` 의 network·web·mcp 검사 186항목. 허용되지 않은 서버에 실제로
296
296
  > 요청이 **한 건도 닿지 않는지**, `--offline` 일 때 MCP 서버가 **한 번도 안
297
297
  > 뜨는지**까지 진짜 서버를 띄워서 확인합니다.
298
298
 
@@ -457,7 +457,7 @@ LM Studio 는 `/api/v0/models`, llama.cpp 는 `/props`. 못 알아보면 `(추
457
457
  |---|---|
458
458
  | `/help` | 명령 목록 |
459
459
  | `/bell [on|off]` | 다 됐을 때·물어볼 때 종소리와 창 제목 |
460
- | `/lang [ko|en|ja|zh] [시킬말]` | 화면 말 (한국어·English·日本語·中文 네 가지). 네 가지 다 채워져 있고, 그 말이 참인지를 **켜서 읽는 검사**가 지킨다 — 일곱 화면을 영어로 띄워 한글이 한 자라도 나오면 빨개진다(`test/langleak.test.js`). 「표는 100% 인데 화면에는 한국어」 를 여기서 한 번 겪었기 때문이다. **시킬 말을 따로 줄 수 있다** — `/lang ko en` 이면 모델에게는 영어로 시키고 답은 한국어로 받는다(8k 창에서 고정 몫 23% 감소). 코드는 그대로 |
460
+ | `/lang [ko|en|ja|zh] [시킬말]` | 화면 말 (한국어·English·日本語·中文 네 가지). 네 가지 다 채워져 있고, 그 말이 참인지를 **켜서 읽는 검사**가 지킨다 — 화면(채팅 안 여섯 · argv 로만 닿는 넷)을 영어로 띄워 한글이 한 자라도 나오면 빨개진다(`test/langleak.test.js`). 채팅창 밖 넷은 아직 한국어가 남아 있어, 0 이 아니라 **줄어들기만 하는 수**로 못박아 두었다. 「표는 100% 인데 화면에는 한국어」 를 여기서 한 번 겪었기 때문이다. **시킬 말을 따로 줄 수 있다** — `/lang ko en` 이면 모델에게는 영어로 시키고 답은 한국어로 받는다(8k 창에서 고정 몫 23% 감소). 코드는 그대로 |
461
461
  | `/keys` | 이 터미널이 무슨 키를 보내는지 눌러서 확인 — 줄바꿈이 안 될 때 |
462
462
  | `/consult <프로필> <질문>` | 다른 모델에게 한 번 물어보기 — 지금 쓰는 것은 안 바꿈 |
463
463
  | `/export` | 이 대화를 **보고서 한 장**(HTML)으로 — 시킨 것·바뀐 것·확인한 것. 폐쇄망에서 여는 자기완결 파일 |
@@ -1087,12 +1087,13 @@ deel sbom --only sbom # SBOM 한 장만
1087
1087
  ## 개발
1088
1088
 
1089
1089
  ```bash
1090
- npm test 전체 검증 (5,890항목 안팎 — 몇몇은 터미널에 따라 갈립니다)
1090
+ npm test 전체 검증 (7,095항목 — 몇몇은 터미널에 따라 갈립니다)
1091
1091
  npm run coverage 검사가 소스의 어디를 밟았는지
1092
1092
  npm run verify 반입·통신 검증만
1093
1093
  npm run bench 편집 성공률 측정
1094
1094
  npm run demo 화면이 어떻게 보이는지 실제로 돌려 보기
1095
- npm run check 전 파일 문법 검사
1095
+ npm run check 전 파일 문법 검사 · 배포에 담길 줄바꿈
1096
+ npm run mutate 검사가 정말 지키는지 되재기 (일부러 어긋내 본다)
1096
1097
  ```
1097
1098
 
1098
1099
  검증은 **가짜 게이트웨이**를 띄워서 합니다. 실제 모델 없이 규격 그대로
@@ -1120,27 +1121,30 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
1120
1121
 
1121
1122
  | 검증 | 항목 | 무엇을 |
1122
1123
  |---|---|---|
1123
- | `smoke` | 20 | 도구·작업범위·되돌리기·감사로그 |
1124
- | `loop` | 16 | 에이전트 루프·스트리밍·도구 호출 |
1125
- | `guard` | 24 | **안 하는 자리** — 거부·모르는 도구·두 번 실행·범위 |
1126
- | `network` | 30 | 정해진 자리 밖으로 새지 않는가 |
1127
- | `web` | 25 | 웹 읽기가 읽기만 하는가 |
1128
- | `abort` · `steer` | 16 · 15 | Ctrl+C 로 끊어도 대화가 성한가 · 도중에 낀 말이 다음 부름에 실리는가 |
1124
+ | `smoke` | 24 | 도구·작업범위·되돌리기·감사로그 |
1125
+ | `loop` | 28 | 에이전트 루프·스트리밍·도구 호출 |
1126
+ | `guard` | 113 | **안 하는 자리** — 거부·모르는 도구·두 번 실행·범위 밖·`.deel` 울타리 |
1127
+ | `network` | 70 | 정해진 자리 밖으로 새지 않는가 |
1128
+ | `web` | 56 | 웹 읽기가 읽기만 하는가 |
1129
+ | `abort` · `steer` | 22 · 15 | Ctrl+C 로 끊어도 대화가 성한가 · 도중에 낀 말이 다음 부름에 실리는가 |
1129
1130
  | `parallel` | 23 | 읽기만 동시에 도는가 · 할 일 목록 |
1130
- | `cli` | 75 | **진짜 `deel` 을 띄워** 끝까지 돌려 본다 |
1131
- | `setup` | 42 | 첫 실행 마법사 (가짜 TTY 로 사람처럼 입력) |
1132
- | `detect` | 66 | 주소 한 줄로 규격·인증을 짚어내는가 |
1133
- | `modes` · `route` | 89 · 33 | 작업 모드 · 종합에서 알맞은 모드로 옮겨 가는가 |
1134
- | `ctxsize` | 43 | 모델에 걸린 컨텍스트 길이를 긁어오는가 |
1135
- | `commands` · `commands-more` | 128 · 62 | 슬래시 명령 전부 |
1136
- | `ui` · `ui2` | 60 · 40 | 암호 가림·한글 폭·상태줄·대화 목록·엑셀→글 |
1137
- | `encoding` · `xlsx` | 68 · 72 | 한글 인코딩 판별 · 엑셀 읽기 |
1138
- | `compact` | 21 | 요약 압축·짝 안 깨짐·실패 시 물러섬 |
1139
- | `store` | 34 | 대화 저장·이어하기·중간에 죽어도 복구 |
1140
- | `scan` | 29 | 여러 런타임을 구분해 찾는가 |
1141
- | `plugins` | 38 | 플러그인 받기·묶기·ZIP/TAR |
1142
- | `no-bundle` | 12 | 배포 묶음에 남의 것이 안 섞였는가 · 검사 파일 위생 |
1131
+ | `cli` · `oneshot` | 84 · 82 | **진짜 `deel` 을 띄워** 끝까지 돌려 본다 · 배치 모드와 종료코드 |
1132
+ | `setup` | 60 | 첫 실행 마법사 (가짜 TTY 로 사람처럼 입력) |
1133
+ | `detect` | 85 | 주소 한 줄로 규격·인증을 짚어내는가 |
1134
+ | `modes` · `route` | 120 · 72 | 작업 모드 · 종합에서 알맞은 모드로 옮겨 가는가 |
1135
+ | `ctxsize` | 56 | 모델에 걸린 컨텍스트 길이를 긁어오는가 |
1136
+ | `commands` · `commands-more` | 214 · 93 | 슬래시 명령 전부 |
1137
+ | `ui` · `ui2` | 89 · 40 | 암호 가림·한글 폭·상태줄·대화 목록·엑셀→글 |
1138
+ | `encoding` · `xlsx` | 70 · 72 | 한글 인코딩 판별 · 엑셀 읽기 |
1139
+ | `compact` | 98 | 요약 압축·짝 안 깨짐·실패 시 물러섬 |
1140
+ | `stuck` · `undo` | 48 · 54 | 헛도는 것과 일이 되는 것을 가르는가 · 셸이 쓴 파일도 되돌려지는가 |
1141
+ | `store` | 75 | 대화 저장·이어하기·중간에 죽어도 복구 |
1142
+ | `scan` | 30 | 여러 런타임을 구분해 찾는가 |
1143
+ | `plugins` | 79 | 플러그인 받기·묶기·ZIP/TAR |
1144
+ | `exitcode` · `doorparity` | 7 · 7 | 화면에 적은 종료코드 표가 진짜인가 · 문 세 개가 같은 것을 주는가 |
1145
+ | `no-bundle` | 21 | 배포 묶음에 남의 것이 안 섞였는가 · 검사 파일 위생 |
1143
1146
  | `edit-bench` | 20건 | 편집 성공률 |
1147
+ | `mutate` | 어긋 7개 | **검사가 정말 지키는가** — 지켜야 할 줄을 일부러 어긋내고 빨개지는지 본다 |
1144
1148
 
1145
1149
  > **자세히** — 어디를 밟았는지 · 폴더 구조
1146
1150
  >
@@ -1152,6 +1156,7 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
1152
1156
 
1153
1157
  | 판 | 무엇이 바뀌었나 |
1154
1158
  |---|---|
1159
+ | **[1.14.0](docs/ko/releases/1.14.md#1140)** | 적어 둔 것이 참인지 기계가 대신 봅니다 — 초록불과 「깨면 잡힌다」 는 다릅니다 |
1155
1160
  | [1.13.1](docs/ko/releases/1.13.md#1131) | 올라간 물건이 소스와 달랐습니다 — 148개 중 47개가 CRLF 로 나갔습니다 |
1156
1161
  | **[1.13.0](docs/ko/releases/1.13.md#1130)** | 바깥 창구와 붙는 자리를 전부 다시 봤습니다 — 서로 못 보는 검수자 셋, 셋이 같이 짚은 것 넷 |
1157
1162
  | **[1.12.0](docs/ko/releases/1.12.md#1120)** | 같은 앞머리를 걸음마다 다시 보내고 있었습니다 |
package/README.md CHANGED
@@ -20,7 +20,7 @@ Vendor APIs connect too — **only when you say so**
20
20
 
21
21
  [![Node.js CI](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/test.yml?branch=main&logo=github&logoColor=white&label=Node.js%20CI)](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
22
22
  [![CodeQL](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/codeql.yml?branch=main&logo=github&logoColor=white&label=CodeQL)](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
23
- [![tests](https://img.shields.io/badge/tests-7%2C046%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/en/develop.md)
23
+ [![tests](https://img.shields.io/badge/tests-7%2C095%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/en/develop.md)
24
24
 
25
25
  [![dependencies](https://img.shields.io/badge/dependencies-0-1a7f37)](https://www.npmjs.com/package/deel-local-cli?activeTab=dependencies)
26
26
  [![ESM](https://img.shields.io/badge/ESM-Node%2020%2B-5FA04E?logo=javascript&logoColor=white)](package.json)
@@ -119,7 +119,7 @@ This page is the **summary**. Each section links to the detail behind it.
119
119
  | [Speed and spend](docs/en/tuning.md) | Per-stage effort · the prefix cache · context length |
120
120
  | [Safety and corporate review](docs/en/safety.md) | Undo · working scope · audit log · the review package |
121
121
  | [Configuration](docs/en/config.md) · [Development](docs/en/develop.md) | Env vars · run flags · running the tests · folder layout |
122
- | [Release notes](docs/en/releases.md) | [1.13.x](docs/en/releases/1.13.md) · [1.12.x](docs/en/releases/1.12.md) · [1.10.x](docs/en/releases/1.10.md) · [1.9.x](docs/en/releases/1.9.md) · [older](docs/en/releases.md) |
122
+ | [Release notes](docs/en/releases.md) | [1.14.x](docs/en/releases/1.14.md) · [1.13.x](docs/en/releases/1.13.md) · [1.12.x](docs/en/releases/1.12.md) · [1.10.x](docs/en/releases/1.10.md) · [1.9.x](docs/en/releases/1.9.md) · [older](docs/en/releases.md) |
123
123
 
124
124
  ---
125
125
 
@@ -235,8 +235,9 @@ What you see on screen does not have to. Four screen languages ship: **한국어
235
235
  > **How this is kept honest:** `/lang` counting the table is not enough — it can only count
236
236
  > strings that made it *into* the table, and for a long time the tool result summaries and the
237
237
  > thinking indicator never did. So a test starts deel in each language and reads the screen:
238
- > `test/langleak.test.js` walks seven screens and fails if a Korean character shows up on an
239
- > English one. It also fails on `1 files`, and on a stray `·` left dangling at the end of a line.
238
+ > `test/langleak.test.js` starts deel on ten screens six inside the chat REPL and four that
239
+ > argv reaches on its own (`--help`, `status`, `sessions`, `reset`) and fails if a Korean
240
+ > character shows up on an English one. It also fails on `1 files`, and on a stray `·` left dangling at the end of a line.
240
241
 
241
242
  ```bash
242
243
  DEEL_LANG=en deel # this run only (also ja, zh)
@@ -337,7 +338,7 @@ deel --offline
337
338
  The destination is printed at the top of every session:
338
339
 
339
340
  ```
340
- deel 1.13.1 ⌂ inside
341
+ deel 1.14.0 ⌂ inside
341
342
  Sends to this machine 127.0.0.1:11434 ← nowhere else
342
343
  ```
343
344
 
@@ -363,7 +364,7 @@ leaves the company."
363
364
  Nothing is collected or transmitted. No telemetry, no usage stats, no crash reporting.
364
365
  Conversation history, undo snapshots and config live only in `.deel/` inside your working folder.
365
366
 
366
- > Verified by 159 checks in `npm test` (network + web + mcp), including bringing up a real
367
+ > Verified by 186 checks in `npm test` (network + web + mcp), including bringing up a real
367
368
  > server and confirming that **not a single request reaches it** when it is not allow-listed,
368
369
  > and that an MCP server **never starts** under `--offline`.
369
370
 
@@ -1157,7 +1158,7 @@ Stored in `~/.deel/config.json`. A `.deel/config.json` in the project folder tak
1157
1158
  ## Development
1158
1159
 
1159
1160
  ```bash
1160
- npm test Full suite (~5,890 checks; a few are TTY-dependent)
1161
+ npm test Full suite (7,095 checks; a few are TTY-dependent)
1161
1162
  npm run coverage Which lines the tests actually execute
1162
1163
  npm run verify Import + network checks only
1163
1164
  npm run bench Edit success rate
@@ -1177,27 +1178,30 @@ so one run tells you everything.
1177
1178
 
1178
1179
  | Suite | Checks | Covers |
1179
1180
  |---|---|---|
1180
- | `smoke` | 20 | Tools, scope, undo, audit log |
1181
- | `loop` | 16 | Agent loop, streaming, tool calls |
1182
- | `guard` | 24 | **What it refuses to do** — denied edits, unknown tools, repeated mutations, out-of-scope writes |
1183
- | `network` | 30 | Nothing escapes the configured address |
1184
- | `web` | 25 | Web reads stay read-only |
1185
- | `abort` · `steer` | 16 · 15 | Ctrl+C leaves the conversation valid · a line typed mid-turn rides the next call |
1181
+ | `smoke` | 24 | Tools, scope, undo, audit log |
1182
+ | `loop` | 28 | Agent loop, streaming, tool calls |
1183
+ | `guard` | 113 | **What it refuses to do** — denied edits, unknown tools, repeated mutations, out-of-scope writes, the `.deel` fence |
1184
+ | `network` | 70 | Nothing escapes the configured address |
1185
+ | `web` | 56 | Web reads stay read-only |
1186
+ | `abort` · `steer` | 22 · 15 | Ctrl+C leaves the conversation valid · a line typed mid-turn rides the next call |
1186
1187
  | `parallel` | 23 | Read-only tools run together; checklists |
1187
- | `cli` | 75 | **Spawns the real `deel`** and drives it to completion |
1188
- | `setup` | 42 | First-run wizard, driven through a fake TTY |
1189
- | `detect` | 66 | Identifying shape and auth from one address |
1190
- | `modes` · `route` | 89 · 33 | Work modes; auto-switching from Auto |
1191
- | `ctxsize` | 43 | Reading context length off the model |
1192
- | `commands` · `commands-more` | 128 · 62 | Every slash command |
1193
- | `ui` · `ui2` | 60 · 40 | Password masking, CJK width, status line, session list, Excel→text |
1194
- | `encoding` · `xlsx` | 68 · 72 | Legacy-encoding detection; Excel reading |
1195
- | `compact` | 21 | Summary folding, pairing intact, graceful fallback |
1196
- | `store` | 34 | Session persistence, resume, crash recovery |
1197
- | `scan` | 29 | Distinguishing multiple runtimes |
1198
- | `plugins` | 38 | Plugin fetch/pack, ZIP/TAR |
1199
- | `no-bundle` | 12 | Nothing foreign in the published package; test-file hygiene |
1188
+ | `cli` · `oneshot` | 84 · 82 | **Spawns the real `deel`** and drives it to completion · batch mode and exit codes |
1189
+ | `setup` | 60 | First-run wizard, driven through a fake TTY |
1190
+ | `detect` | 85 | Identifying shape and auth from one address |
1191
+ | `modes` · `route` | 120 · 72 | Work modes; auto-switching from Auto |
1192
+ | `ctxsize` | 56 | Reading context length off the model |
1193
+ | `commands` · `commands-more` | 214 · 93 | Every slash command |
1194
+ | `ui` · `ui2` | 89 · 40 | Password masking, CJK width, status line, session list, Excel→text |
1195
+ | `encoding` · `xlsx` | 70 · 72 | Legacy-encoding detection; Excel reading |
1196
+ | `compact` | 98 | Summary folding, pairing intact, graceful fallback |
1197
+ | `stuck` · `undo` | 48 · 54 | Telling spinning apart from real progress · shell-written files are undoable too |
1198
+ | `store` | 75 | Session persistence, resume, crash recovery |
1199
+ | `scan` | 30 | Distinguishing multiple runtimes |
1200
+ | `plugins` | 79 | Plugin fetch/pack, ZIP/TAR |
1201
+ | `exitcode` · `doorparity` | 7 · 7 | The printed exit-code table is real · all three doors hand out the same thing |
1202
+ | `no-bundle` | 21 | Nothing foreign in the published package; test-file hygiene |
1200
1203
  | `edit-bench` | 20 cases | Edit success rate |
1204
+ | `mutate` | 7 mutants | **Whether the tests actually guard** — break the line on purpose, check it turns red |
1201
1205
 
1202
1206
  > **More** — Coverage · Layout
1203
1207
  >
@@ -1209,6 +1213,7 @@ so one run tells you everything.
1209
1213
 
1210
1214
  | Version | What changed |
1211
1215
  |---|---|
1216
+ | **[1.14.0](docs/en/releases/1.14.md#1140)** | A machine now checks that what we wrote down is true — green does not mean guarded |
1212
1217
  | [1.13.1](docs/en/releases/1.13.md#1131) | What shipped was not what the source said — 47 of 148 files went out with CRLF line endings |
1213
1218
  | **[1.13.0](docs/en/releases/1.13.md#1130)** | Every place deel touches someone else's endpoint, re-examined — three independent reviewers, four findings they shared |
1214
1219
  | **[1.12.0](docs/en/releases/1.12.md#1120)** | The same prefix was being sent again on every step |
package/bin/deel.js CHANGED
@@ -242,13 +242,18 @@ function help() {
242
242
  say('');
243
243
  say(` ${c.cyan('deel run "검사 돌리고 실패한 것만 알려줘"')}`);
244
244
  say(` ${c.cyan('echo "..." | deel run')} ${c.gray('시킬 말을 표준입력으로 넣어도 됩니다')}`);
245
+ // 대화 화면에서 손으로 치던 슬래시 명령을 그대로 배치에 옮기는 자리 (src/oneshot.js).
246
+ say(` ${c.cyan('deel run /배포점검 서버3')} ${c.gray('.claude/commands 에 적어 둔 슬래시 명령도 그대로')}`);
245
247
  say(` ${c.gray('deel -p "..." 도 같습니다.')} ${c.gray('위 대화 시작 옵션을 그대로 씁니다.')}`);
246
248
  say('');
247
249
  say(` ${c.gray('--json')} 결과를 JSON 한 덩이로 (답·도구 횟수·토큰·끝난 까닭)`);
248
250
  say(` ${c.gray('--quiet')} 도구가 무엇을 했는지 안 적음 (오류는 그래도 적음)`);
249
251
  say(` ${c.gray('--yes')} 승인이 필요한 것도 그냥 실행. ${c.yellow('기본은 거부입니다')}`);
250
252
  say(` ${c.gray('답은 표준출력, 도구 기록은 표준오류로 나갑니다 — 파이프로 넘겨도 답만 넘어갑니다.')}`);
251
- say(` ${c.gray('끝난 까닭이 종료코드에 담깁니다:')} ${c.gray('0 끝냄 · 1 오류 · 2 걸음수상한 · 3 헛돎 · 4 중단 · 5 말없이끊김')}`);
253
+ // 줄은 src/oneshot.js EXIT 짝이다. test/exitcode.test.js 둘이
254
+ // 어긋나면 빨개진다 — 한 판 동안 refusal(6) 이 여기서 빠져 있었고, 그
255
+ // 사실을 말해 주는 자리가 아무 데도 없었다.
256
+ say(` ${c.gray('끝난 까닭이 종료코드에 담깁니다:')} ${c.gray('0 끝냄 · 1 오류 · 2 걸음수상한 · 3 헛돎 · 4 중단 · 5 말없이끊김 · 6 거절')}`);
252
257
  say('');
253
258
  say(` ${c.bold('진단 직접 지정')} ${c.gray('— 설정을 남기지 않고 확인만 할 때')}`);
254
259
  say('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deel-local-cli",
3
- "version": "1.13.1",
3
+ "version": "1.14.0",
4
4
  "description": "A coding-agent CLI that runs on local models and private gateways. Zero dependencies, Node 20+.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -48,11 +48,12 @@
48
48
  "chat": "node bin/deel.js",
49
49
  "test": "node test/run.mjs",
50
50
  "docs": "node tools/check-docs.mjs",
51
- "verify": "node test/no-bundle.test.js && node test/network.test.js && node test/web.test.js",
51
+ "verify": "node test/no-bundle.test.js && node test/network.test.js && node test/web.test.js && node test/mcp.test.js",
52
52
  "bench": "node test/edit-bench.js",
53
53
  "demo": "node test/demo.js",
54
54
  "check": "node tools/check-src.mjs",
55
55
  "coverage": "node test/coverage.mjs",
56
+ "mutate": "node tools/mutate.mjs",
56
57
  "prepublishOnly": "npm run check && npm test"
57
58
  },
58
59
  "dependencies": {},
package/src/acp/serve.js CHANGED
@@ -30,7 +30,8 @@
30
30
  import { existsSync } from 'node:fs';
31
31
  import { join } from 'node:path';
32
32
  import { VERSION } from '../version.js';
33
- import { 규칙모으기, 늘허락 } from '../safety/policy.js';
33
+ import { 규칙모으기, 늘허락, 정책읽기 } from '../safety/policy.js';
34
+ import { 받기설정 } from '../safety/authcmd.js';
34
35
  import { run } from '../agent/loop.js';
35
36
  import { Session, repairToolPairs } from '../agent/session.js';
36
37
  import { Store, sessionsDir, prune } from '../agent/store.js';
@@ -139,6 +140,25 @@ export async function acp(opts = {}) {
139
140
  maxTokens: opts.maxTokens ?? prof.maxTokens ?? null,
140
141
  streaming: prof.streaming ?? false,
141
142
  tools: prof.tools ?? false, json: prof.json ?? false, think: prof.think ?? false,
143
+ /*
144
+ * ── 문이 셋인데 둘만 이걸 넣고 있었다 ──────────────────────────
145
+ *
146
+ * repl.js 와 oneshot.js 는 이 두 열쇠를 넣는데 여기만 안 넣고 있었다.
147
+ * 같은 프로필로 같은 게이트웨이에 붙는데 **들어온 문에 따라 다른
148
+ * 프로그램**이 되던 자리다.
149
+ *
150
+ * vision : 없으면 아래 눈있나 게터가 늘 거짓이 되어, 그림을 볼 수
151
+ * 있는 모델에 붙어 있어도 「이 모델은 그림을 못 봅니다」
152
+ * 라고 답한다. 모델 얘기인데 우리 쪽 사실이 틀렸다.
153
+ * 열쇠받기 : 없으면 401 을 받고 열쇠를 다시 받아 오는 길이 잠긴다
154
+ * (backend/adapter.js 가 !!conn.열쇠받기 로 잠근다).
155
+ * 한 시간짜리 토큰을 쓰는 사내 게이트웨이가 그 꼴이다 —
156
+ * 터미널에서는 되고 에디터에서는 한 시간 뒤에 죽는다.
157
+ *
158
+ * test/doorparity.test.js 가 세 문의 열쇠 집합을 맞춰 본다.
159
+ */
160
+ vision: prof.vision ?? false,
161
+ 열쇠받기: 받기설정(prof, { 정책값: 정책읽기().값 }),
142
162
  };
143
163
  /*
144
164
  * 바깥으로 나가는 연결은 허가가 있어야 연다 (safety/runmode.js).
package/src/agent/loop.js CHANGED
@@ -287,6 +287,30 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
287
287
  * 있는 것이라 되풀이 셈을 처음부터 다시 센다 — 까닭은 세는 자리에.
288
288
  */
289
289
  const 반복진척 = new Map();
290
+ /*
291
+ * 자리가 줄어든 걸음에서는 위 두 표를 **통째로 버린다.**
292
+ *
293
+ * 두 표는 「그 결과가 대화에 아직 있다」 를 전제로 다시 안 싣는다. 접기·요약·
294
+ * 비우기는 그 전제를 깨는 일이다 — 원문을 없애 놓고 표만 남기면, 같은 파일을
295
+ * 다시 읽어도 「앞에서 부른 것과 인자도 결과도 같습니다」 가 돌아간다. 모델이
296
+ * 받는 것은 대화 어디에도 없는 글을 가리키는 쪽지고, 파일 내용은 영영 안
297
+ * 돌아온다. 오류는 한 줄도 안 나면서 하던 일이 그 자리에서 멈춘다.
298
+ * 접을 때 파일 기억(agent/filemem.js)을 같이 지우는 것과 똑같은 까닭이다.
299
+ *
300
+ * 접힌 것만 골라 지울 수는 없다. 표의 열쇠는 부름 서명(이름+인자 JSON)인데
301
+ * 접기가 돌려주는 것은 도구 이름과 경로뿐이라, Bash·Grep 처럼 경로가 없는
302
+ * 부름은 짝을 못 맞춘다. 통째로 버리는 쪽이 안전한 방향이다 — 잘못해야
303
+ * 결과 하나를 두 번 싣는 것이고 그건 접기가 다시 걷어 간다. 반대쪽 잘못은
304
+ * 턴이 죽는 것이다. 헛도는 것은 걸음 수 상한이 여전히 막는다.
305
+ *
306
+ * 되풀이 셈(막힘의 `반복|…`)도 같이 버린다. 표만 지우고 셈을 남기면, 접힌
307
+ * 뒤 처음 읽은 것이 이미 두 번째로 세어져 한 걸음 일찍 멈춘다.
308
+ */
309
+ const 되풀이표잊기 = () => {
310
+ 부른것.clear();
311
+ 반복진척.clear();
312
+ for (const k of [...막힘.keys()]) if (k.startsWith('반복|')) 막힘.delete(k);
313
+ };
290
314
  /*
291
315
  * 서명은 한 모양으로 맞춰 놓고 견준다 (NFC).
292
316
  *
@@ -574,6 +598,8 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
574
598
  session.파일기억?.잊기(열쇠);
575
599
  if (열쇠 !== x.경로) session.파일기억?.잊기(x.경로);
576
600
  }
601
+ // 접힌 원문을 「앞에서 봤잖아」 로 대신하지 않게 — 까닭은 되풀이표잊기 머리말에.
602
+ 되풀이표잊기();
577
603
  yield { type: 'folded', ...f };
578
604
  }
579
605
  // 그림은 사람 말 자리에 실려서 위 접기가 못 건드린다. 따로 뺀다.
@@ -589,7 +615,7 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
589
615
  for await (const 알림 of 편지.소식()) yield 알림;
590
616
  const r = await 편지.부름;
591
617
  if (r.aborted) { 접다멈췄나 = true; yield { type: 'aborted', steps, kept: true }; return; }
592
- if (r.ok) yield { type: 'compacted', ...r };
618
+ if (r.ok) { 되풀이표잊기(); yield { type: 'compacted', ...r }; }
593
619
  else yield { type: 'compact_failed', why: r.why };
594
620
  }
595
621
  };
@@ -952,6 +978,7 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
952
978
  }];
953
979
  // 내용은 없어졌는데 '읽었다' 는 표만 남으면 모델이 다시 안 읽는다.
954
980
  try { session.filesRead?.clear?.(); session.파일기억?.잊기?.(); } catch { /* 없어도 그만 */ }
981
+ 되풀이표잊기();
955
982
  yield {
956
983
  type: 'reset',
957
984
  dropped: 지운수,
@@ -0,0 +1,68 @@
1
+ // 슬래시 명령 이름표. 이름과 인자 유무만 — 사람이 읽는 글은 i18n 쪽에 있다.
2
+ //
3
+ // ── 왜 파일이 따로인가 ──────────────────────────────────────────────────
4
+ //
5
+ // 이 표를 보고 싶은 자리가 commands.js 바깥에도 있다. `deel run /이름` 이
6
+ // 그렇다 — 비대화 모드는 「이 이름이 대화 화면 전용인가」 를 알아야 "모르는
7
+ // 명령" 대신 "그건 대화 화면에서만 됩니다" 라고 말해 줄 수 있다.
8
+ //
9
+ // 그렇다고 oneshot.js 가 commands.js 를 부를 수는 없다. 그쪽은 대화 화면
10
+ // 한 벌을 통째로 끌고 온다 — 미리보기 서버 · 클립보드 · 화면 그리기까지.
11
+ // 배치로 도는 길에 그것이 딸려 들어오면 켜는 데만 시간이 늘고, 언젠가 그
12
+ // 안의 무엇이 사람을 기다리기 시작하면 배치가 그 자리에서 선다. 이 파일이
13
+ // 들여오는 것이 하나도 없는 것은 그래서다 — 표는 그냥 표다.
14
+ //
15
+ // desc·arg 를 게터로 만드는 일(COMMANDS)은 commands.js 에 그대로 뒀다.
16
+ // 그쪽은 i18n 을 부르므로 여기 두면 이 파일이 더는 순수한 표가 아니다.
17
+
18
+ export const 명령들 = {
19
+ help: { arg: false },
20
+ clear: { arg: false },
21
+ context: { arg: false },
22
+ ctx: { arg: true },
23
+ out: { arg: true },
24
+ grade: { arg: true },
25
+ compact: { arg: false },
26
+ model: { arg: true },
27
+ think: { arg: true },
28
+ mode: { arg: true },
29
+ work: { arg: true },
30
+ auto: { arg: false },
31
+ code: { arg: false },
32
+ plan: { arg: false },
33
+ architect: { arg: false },
34
+ debug: { arg: false },
35
+ ask: { arg: false },
36
+ orchestrator: { arg: false },
37
+ undo: { arg: true },
38
+ diff: { arg: true },
39
+ preview: { arg: true },
40
+ tools: { arg: false },
41
+ skills: { arg: true },
42
+ plugin: { arg: true },
43
+ cost: { arg: false },
44
+ status: { arg: false },
45
+ scan: { arg: true },
46
+ thread: { arg: true },
47
+ learned: { arg: true },
48
+ pin: { arg: true },
49
+ evidence: { arg: true },
50
+ commit: { arg: true },
51
+ review: { arg: false },
52
+ paste: { arg: false },
53
+ sessions: { arg: false },
54
+ recall: { arg: true },
55
+ mcp: { arg: false },
56
+ memory: { arg: true },
57
+ level: { arg: true },
58
+ bell: { arg: true },
59
+ keys: { arg: false },
60
+ motion: { arg: true },
61
+ consult: { arg: true },
62
+ lang: { arg: true },
63
+ lsp: { arg: true },
64
+ export: { arg: true },
65
+ init: { arg: false },
66
+ exit: { arg: false },
67
+ quit: { arg: false },
68
+ };
package/src/commands.js CHANGED
@@ -69,81 +69,13 @@ import { readTextFull } from './tools/fsutil.js';
69
69
  import { 띄우기, 브라우저로 } from './preview/serve.js';
70
70
  import { 클립보드그림, 그림앉히기 } from './tools/clipboard.js';
71
71
  import { 크기말 } from './backend/vision.js';
72
+ import { 명령들 } from './cmdnames.js';
72
73
  const MODES = {
73
74
  auto: '자율 — 전부 알아서. 되돌리기가 안전망',
74
75
  confirm: '확인 — 되돌릴 수 없는 것만 물어봄',
75
76
  strict: '엄격 — 파일 변경·명령 전부 물어봄',
76
77
  };
77
78
 
78
- /*
79
- * 슬래시 명령 목록.
80
- *
81
- * ── 왜 설명이 여기 없나 ─────────────────────────────────────────────────
82
- *
83
- * 이름과 인자 유무만 여기 두고, **사람이 읽는 글은 i18n/ko.js 에 있다.**
84
- * 명령 이름은 원래부터 영어였는데(help·model·undo…) 그 옆의 설명이 전부
85
- * 한글이라, 저장소를 처음 연 영어권 사람은 `/help` 를 쳐 봐도 무엇을 하는
86
- * 명령인지 알 수가 없었다. 그래서 설명만 갈아 끼울 수 있게 뺐다.
87
- *
88
- * desc·arg 를 **게터로** 둔다. 켤 때 한 번 읽어 굳히면 /lang 으로 바꿔도
89
- * 목록이 안 따라오고, 그러면 바뀐 것처럼 보이다가 안 바뀐 자리가 남는다.
90
- * 볼 때마다 지금 언어로 읽는 편이 어긋날 자리가 없다.
91
- *
92
- * 없는 열쇠는 열쇠 이름이 그대로 나온다 — 빈칸이 아니다. 명령을 새로 넣고
93
- * ko.js 에 적는 것을 빠뜨리면 `cmd.foo.desc` 가 화면에 찍혀서 바로 눈에 띈다.
94
- * (검사도 그걸 잡는다.)
95
- */
96
- const 명령들 = {
97
- help: { arg: false },
98
- clear: { arg: false },
99
- context: { arg: false },
100
- ctx: { arg: true },
101
- out: { arg: true },
102
- grade: { arg: true },
103
- compact: { arg: false },
104
- model: { arg: true },
105
- think: { arg: true },
106
- mode: { arg: true },
107
- work: { arg: true },
108
- auto: { arg: false },
109
- code: { arg: false },
110
- plan: { arg: false },
111
- architect: { arg: false },
112
- debug: { arg: false },
113
- ask: { arg: false },
114
- orchestrator: { arg: false },
115
- undo: { arg: true },
116
- diff: { arg: true },
117
- preview: { arg: true },
118
- tools: { arg: false },
119
- skills: { arg: true },
120
- plugin: { arg: true },
121
- cost: { arg: false },
122
- status: { arg: false },
123
- scan: { arg: true },
124
- thread: { arg: true },
125
- learned: { arg: true },
126
- pin: { arg: true },
127
- evidence: { arg: true },
128
- commit: { arg: true },
129
- review: { arg: false },
130
- paste: { arg: false },
131
- sessions: { arg: false },
132
- recall: { arg: true },
133
- mcp: { arg: false },
134
- memory: { arg: true },
135
- level: { arg: true },
136
- bell: { arg: true },
137
- keys: { arg: false },
138
- motion: { arg: true },
139
- consult: { arg: true },
140
- lang: { arg: true },
141
- lsp: { arg: true },
142
- export: { arg: true },
143
- init: { arg: false },
144
- exit: { arg: false },
145
- quit: { arg: false },
146
- };
147
79
 
148
80
  /**
149
81
  * 화면에 낼 명령표. desc·arg 는 볼 때마다 지금 언어로 읽는다.
package/src/oneshot.js CHANGED
@@ -24,7 +24,8 @@ import { 말 as 옮긴말 } from './i18n/index.js';
24
24
  import { 알림채움 } from './backend/retry.js';
25
25
  import { 전선붙이기, 세션이름짓기 } from './backend/wire.js';
26
26
  import { newId } from './agent/store.js';
27
- import { discover } from './skills/discover.js';
27
+ import { discover, loadCommand } from './skills/discover.js';
28
+ import { 명령들 } from './cmdnames.js';
28
29
  import { allowEndpoint, setOffline } from './safety/network.js';
29
30
  import { 지금모드, 바깥인가, 나갈수있나 } from './safety/runmode.js';
30
31
  import { 주소가리기 } from './safety/secrets.js';
@@ -259,6 +260,65 @@ export async function runOnce(opts = {}) {
259
260
  session.commands = found.commands;
260
261
  session.plugins = found.plugins;
261
262
 
263
+ /*
264
+ * ── `deel run /이름` ──────────────────────────────────────────────────
265
+ *
266
+ * 사람이 대화 화면에서 쓰던 슬래시 명령을 배치에서도 그대로 부르게 한다.
267
+ * `.claude/commands/배포점검.md` 를 만들어 두고 아침마다 손으로 `/배포점검`
268
+ * 을 치고 있었으면, 그걸 야간 잡에 옮기는 데 드는 일이 이것 하나다.
269
+ *
270
+ * 여태는 `/배포점검` 이 **그냥 글자로** 모델에게 갔다. 슬래시가 붙은 낱말
271
+ * 하나를 받은 모델은 대개 "무슨 뜻인지 모르겠다" 고 답하고 0 으로 끝난다 —
272
+ * 아무 일도 안 했는데 배치는 초록불이다. 이 파일이 제일 피하려는 결말이다.
273
+ *
274
+ * 세 갈래로 끝난다.
275
+ * · 이 PC 에 파일로 있는 명령이면 → 본문을 펴서 그것을 시킨다
276
+ * · 대화 화면 전용 이름(/help·/undo…)이면 → 여기선 안 된다고 말하고 1
277
+ * · 아무것도 아니면 → 모르는 명령이라고 말하고 1
278
+ * 두 실패 모두 **종료코드가 0 이 아니다.** 스크립트가 보는 것은 그 숫자다.
279
+ *
280
+ * 첫 낱말에 슬래시·역슬래시가 또 있으면 명령이 아니라 경로다(`/mnt/d/일감`).
281
+ * 그건 건드리지 않고 시킨 말 그대로 둔다 — commands.js 의 경로처럼보이나 와
282
+ * 같은 기준이다.
283
+ */
284
+ if (시킬말.startsWith('/')) {
285
+ const [부른이름 = '', ...나머지] = 시킬말.slice(1).trim().split(/\s+/);
286
+ const 경로인가 = 부른이름.includes('/') || 부른이름.includes('\\');
287
+ if (부른이름 && !경로인가) {
288
+ const 인자 = 나머지.join(' ');
289
+ const 낮춘 = 부른이름.toLowerCase();
290
+ /*
291
+ * 붙박이 이름을 **먼저** 본다 — 대화 화면과 차례가 같아야 한다.
292
+ *
293
+ * commands.js 는 switch 로 붙박이를 먼저 받고, 못 받은 것만 default 에서
294
+ * 찾아 쓴다. 그래서 대화 화면의 `/help` 는 언제나 도움말이다. 여기서
295
+ * 차례를 뒤집으면 이 PC 에 `ralph-loop:help` 같은 플러그인 명령이 깔린
296
+ * 순간 `deel run /help` 만 딴 것을 부른다 — 같은 글자가 창에 따라 다른
297
+ * 일을 하는 것이 제일 나쁘다. 깔린 것에 따라 뜻이 흔들리면 안 된다.
298
+ */
299
+ const 붙박이 = !!명령들[낮춘];
300
+ // 찾는 차례는 대화 화면과 같다 — 적은 그대로 · 대소문자 무시 · 꼬리 이름.
301
+ const 찾은 = 붙박이 ? null : (found.commands.find((x) => x.name === 부른이름)
302
+ ?? found.commands.find((x) => x.name.toLowerCase() === 낮춘)
303
+ ?? found.commands.find((x) => x.name.split(':').pop() === 낮춘));
304
+ if (붙박이) {
305
+ return 못함('repl-only', `/${부른이름} 은 대화 화면에서만 도는 명령입니다.`
306
+ + ' 여기서 되는 것은 파일로 적어 둔 슬래시 명령뿐입니다 (.claude/commands · .deel/commands).');
307
+ } else if (찾은) {
308
+ const { text, error } = loadCommand(찾은, 인자);
309
+ if (error) return 못함('command-read', `/${부른이름} 을 읽지 못했습니다 — ${error}`);
310
+ 곁(` ${c.cyan('⌘')} ${찾은.name} ${c.gray(찾은.source)}`);
311
+ 시킬말 = text;
312
+ } else {
313
+ const 비슷 = found.commands
314
+ .filter((x) => x.name.includes(낮춘) || 낮춘.includes(x.name.split(':').pop()))
315
+ .slice(0, 5).map((x) => '/' + x.name);
316
+ return 못함('no-command', `모르는 명령 /${부른이름}`
317
+ + (비슷.length ? ` — 비슷한 것: ${비슷.join(' ')}` : ' — 이 폴더에서 찾은 슬래시 명령이 없습니다.'));
318
+ }
319
+ }
320
+ }
321
+
262
322
  // ── 물어보는 자리를 전부 막는다 ────────────────────────────────────────
263
323
  //
264
324
  // 여기가 이 파일의 핵심이다. 물어보는 함수 하나라도 기다리게 두면 그것 하나로
@@ -199,7 +199,7 @@ export function reviewSheet(a, at, { lang = 언어() } = {}) {
199
199
  L.push(' 아예 띄우지 않습니다.');
200
200
  L.push('');
201
201
  L.push(' --offline 으로 켜면 [B]·[C]·[D] 가 모두 막히고, 이 컴퓨터 안으로만 다닙니다.');
202
- L.push(' (검증: npm test 안의 network / web / mcp 검사 123항목이 이를 확인합니다)');
202
+ L.push(' (검증: npm test 안의 network / web / mcp 검사가 이를 확인합니다 — 항목 수는 그 출력에 있습니다)');
203
203
  L.push('');
204
204
 
205
205
  L.push('4. 스킬·플러그인');
@@ -209,7 +209,20 @@ export function reviewSheet(a, at, { lang = 언어() } = {}) {
209
209
  L.push(' (검증: npm test 안의 no-bundle 검사가 이를 확인합니다)');
210
210
  L.push('');
211
211
 
212
- L.push(`5. 담긴 파일 ${a.files.length}개 — SHA-256`);
212
+ L.push('5. 검사가 무엇을 지키는가');
213
+ L.push(줄());
214
+ L.push(' 검사가 초록이라는 것은 「아무도 안 깨졌다」 는 뜻이지 「깨면 잡힌다」 는 뜻이');
215
+ L.push(' 아닙니다. 이 둘은 겉으로 구별이 안 되는데 값어치는 정반대입니다.');
216
+ L.push(' 그래서 지켜야 할 줄을 일부러 어긋내 놓고, 짝지은 검사가 빨개지는지 되잽니다.');
217
+ L.push(' 무엇을 어긋내는지와 「그러면 무슨 일이 나는지」 는 test/mutants.json 에 적어');
218
+ L.push(' 두었습니다 — 살림 폴더 통째로 지우기 · 되돌리기 스냅샷 · ACP 문 파리티 ·');
219
+ L.push(' 도움말의 종료코드 표가 그 안에 있습니다. 하나라도 안 잡히면 실패입니다.');
220
+ L.push(' (검증: npm run mutate — 잡은 수와 샌 수가 그 출력에 있습니다)');
221
+ L.push(' 검사와 이 목록은 이 묶음에 안 들어 있습니다 — 묶음에는 도는 코드만 담습니다.');
222
+ L.push(' 저장소에서 보실 수 있습니다: https://github.com/jysvai/deel-local-cli');
223
+ L.push('');
224
+
225
+ L.push(`6. 담긴 파일 ${a.files.length}개 — SHA-256`);
213
226
  L.push(줄());
214
227
  const w = Math.max(...a.files.map((f) => f.path.length));
215
228
  for (const f of a.files) {
@@ -106,7 +106,7 @@ export function reviewSheetEn(a, at, probes) {
106
106
  L.push(' filtering its requests, --offline does not start the server at all.');
107
107
  L.push('');
108
108
  L.push(' Starting with --offline blocks [B], [C] and [D]; traffic stays on this machine.');
109
- L.push(' (Verified by the network / web / mcp checks in npm test - 123 assertions.)');
109
+ L.push(' (Verified by the network / web / mcp checks in npm test; the count is in that output.)');
110
110
  L.push('');
111
111
 
112
112
  L.push('4. Skills and plugins');
@@ -116,7 +116,21 @@ export function reviewSheetEn(a, at, probes) {
116
116
  L.push(' (Verified by the no-bundle check in npm test.)');
117
117
  L.push('');
118
118
 
119
- L.push(`5. ${a.files.length} files shipped - SHA-256`);
119
+ L.push('5. What the tests actually guard');
120
+ L.push(rule());
121
+ L.push(' A green test run means "nothing broke", not "breaking it would be caught".');
122
+ L.push(' The two look identical and are worth the opposite.');
123
+ L.push(' So we deliberately break the lines that matter and check that the paired test');
124
+ L.push(' turns red. What gets broken, and what would go wrong if it stayed broken, is');
125
+ L.push(' written down in test/mutants.json - deleting the whole .deel folder, undo');
126
+ L.push(' snapshots, ACP door parity, and the exit-code table are among them.');
127
+ L.push(' A single survivor fails the run.');
128
+ L.push(' (Verified by npm run mutate; killed and survived counts are in that output.)');
129
+ L.push(' The tests and that list are not in this package - it ships only running code.');
130
+ L.push(' Read them in the repository: https://github.com/jysvai/deel-local-cli');
131
+ L.push('');
132
+
133
+ L.push(`6. ${a.files.length} files shipped - SHA-256`);
120
134
  L.push(rule());
121
135
  const w = Math.max(...a.files.map((f) => f.path.length));
122
136
  for (const f of a.files) {
@@ -466,7 +466,25 @@ export function 경로낱말(cmd) {
466
466
  t = t.replace(/^[<>]+/, '');
467
467
  if (!t) continue;
468
468
  const 풀린 = 자리표풀기(t);
469
- const 경로같나 = 풀린.includes('/') || 풀린.includes('\\') || /^[a-zA-Z]:$/.test(풀린);
469
+ /*
470
+ * ── 맨 낱말 `.deel` 도 자리로 친다 ──────────────────────────────────
471
+ *
472
+ * 이 필터는 `/` 나 `\` 가 있어야 「자리」 로 쳤다. 그래서 `rm -rf .deel`
473
+ * 의 `.deel` 은 **후보로도 안 잡혔다.** checkPaths 는 이 목록 안에서만
474
+ * 내부살림() 을 부르므로, 내부살림 을 아무리 고쳐도 이 철자는 안 걸린다.
475
+ *
476
+ * 그 결과가 이랬다 — 같은 파일을 지우는 두 명령이 서로 다른 답을 받았다.
477
+ *
478
+ * rm -rf .deel/history → 막힘
479
+ * rm -rf .deel → 통과
480
+ *
481
+ * 그래서 이 이름 하나만 예외로 둔다. **품고 있는 것이 아니라 그 이름
482
+ * 자체**일 때만이다 — `.deelignore` 는 다른 파일이고, `my.deel.txt` 도
483
+ * 남의 파일이다. 넓히다 반대로 베면 모델이 까닭 없이 무언가를 못 하게
484
+ * 되는데, 그것을 신고해 줄 사람이 없다.
485
+ */
486
+ const 살림이름 = 풀린 === '.deel' || 풀린.toLowerCase() === '.deel';
487
+ const 경로같나 = 풀린.includes('/') || 풀린.includes('\\') || /^[a-zA-Z]:$/.test(풀린) || 살림이름;
470
488
  if (!경로같나) continue;
471
489
  /*
472
490
  * 남은 자리표가 있으면(우리가 못 푼 것) **어디로 갈지 모른다.** 울타리
@@ -639,3 +657,58 @@ const MUTATING = /(^|[|;&]\s*)(sudo\s+)?(git\s+(commit|push|merge|rebase|reset)|
639
657
  export function isMutating(cmd) {
640
658
  return MUTATING.test(String(cmd));
641
659
  }
660
+
661
+ /*
662
+ * 셸이 **파일에 쓰는 꼴**인가 — 스냅샷을 뜰지 정하는 데만 쓴다.
663
+ *
664
+ * ── 왜 MUTATING 을 넓히지 않고 새로 만드나 ──────────────────────────────
665
+ *
666
+ * MUTATING 은 세 곳에서 쓰인다.
667
+ *
668
+ * tools/index.js:307 스냅샷을 뜰지 ← 넓히고 싶은 곳
669
+ * agent/loop.js:1214 같은 명령을 두 번 안 돌린다
670
+ * agent/loop.js:1255 엄격 모드에서 물어본다
671
+ *
672
+ * 아래 것들을 MUTATING 에 넣으면 뒤의 두 곳이 같이 넓어진다. `echo x > log`
673
+ * 마다 승인 창이 뜨고, 두 번째 `echo` 가 「다시 실행하지 않습니다」 로 막힌다.
674
+ * 고치려던 것보다 나쁜 것이 생긴다. 그래서 **쓰임이 하나뿐인 술어**를 따로 둔다.
675
+ *
676
+ * ── 왜 넉넉해도 되나 ────────────────────────────────────────────────────
677
+ *
678
+ * 여기서 잘못 걸리면 안 떠도 될 파일을 하나 더 뜨는 것뿐이다. MUTATING 에서
679
+ * 잘못 걸리면 멀쩡한 명령이 막힌다. 값이 다른 두 실수라, 이쪽은 넉넉히 잡는다.
680
+ * `echo "a > b"` 처럼 글 안의 부등호까지 걸려도 손해가 스냅샷 한 장이다.
681
+ *
682
+ * ── 왜 이 목록이 필요했나 ───────────────────────────────────────────────
683
+ *
684
+ * 열한 가지 파일 고치기를 재 보니 MUTATING 에는 `rm` 하나만 걸렸다.
685
+ *
686
+ * O rm f.js
687
+ * X echo x > f.js X printf x >> f.js X sed -i s/a/b/ f.js
688
+ * X tee f.js X truncate -s 0 f.js X dd of=f.js
689
+ * X node -e "…" X python -c "…" X npx prettier --write src/
690
+ * X git checkout -- f.js
691
+ *
692
+ * 즉 모델이 셸에서 파일을 고치는 **흔한 방법 전부**가 스냅샷 없이 지나갔다.
693
+ * 그리고 스냅샷이 없으면 그 턴은 기록을 한 줄도 안 남겨서, /undo 가 그 턴의
694
+ * 존재를 모르고 **앞의 무관한 턴**을 되돌린 뒤 성공했다고 적었다.
695
+ * (검사: test/undo.test.js 의 「셸쓰기-다른턴을-되돌리던-것」)
696
+ */
697
+ const 셸쓰기무늬 = [
698
+ />{1,2}\s*(?![&>])\S/, // echo x > f · printf x >> f (2>&1 은 & 라 안 걸린다)
699
+ /\bsed\s+(-[a-z]*i\b|--in-place)/i, // sed -i · sed --in-place
700
+ /\bperl\s+(-[a-z]*i\b|--in-place)/i,
701
+ /\btee\b/i,
702
+ /\b(set-content|add-content|out-file)\b/i, // 파워셸
703
+ /\btruncate\b/i,
704
+ /\bdd\b[^|;&]*\bof=/i,
705
+ /--write\b/i, // prettier --write 류
706
+ /\bpatch\b/i,
707
+ /\bgit\s+(checkout\s+--|restore)\b/i, // 작업 트리를 되돌리는 것도 파일을 바꾼다
708
+ ];
709
+
710
+ /** 이 명령이 파일 내용을 바꿀 낌새인가. **스냅샷 판단에만** 쓴다. */
711
+ export function 셸이파일에쓰나(cmd) {
712
+ const s = String(cmd ?? '');
713
+ return 셸쓰기무늬.some((무늬) => 무늬.test(s));
714
+ }
@@ -112,6 +112,27 @@ export function 내부살림(abs) {
112
112
  }
113
113
  if (i >= 0) {
114
114
  const 안 = 조각.slice(i + 1);
115
+ /*
116
+ * ── 폴더 **자체**를 가리키면 그것도 막는다 ──────────────────────────
117
+ *
118
+ * 여태 이 자는 「그 안의 파일」 만 봤다. `안` 이 비면(경로가 살림 뿌리
119
+ * 자체면) 아무 갈래에도 안 걸리고 그대로 null 로 떨어졌다. 그래서
120
+ * `rm -rf .deel/history` 는 막히는데 `rm -rf .deel` 은 통과했다 —
121
+ * 같은 파일을 지우는 두 명령이 서로 다른 답을 받았다.
122
+ *
123
+ * 이 폴더에는 되돌리기 이력과 감사 기록이 산다. 되돌리기는 이 프로그램이
124
+ * 승인 창을 안 띄우는 **근거**다(집안 규칙 3). 그 그물을 걷어 내는
125
+ * 명령을 그냥 통과시키면 근거가 통째로 없어지고, 그 사실은 되돌리려고
126
+ * 할 때에야 드러난다.
127
+ *
128
+ * 「작업 폴더 정리해 줘」 한 마디에 `rm -rf build dist .deel` 이 나오는
129
+ * 것은 드문 일이 아니다.
130
+ */
131
+ if (안.length === 0) {
132
+ return 'deel 자신의 살림 폴더입니다. 되돌리기 이력과 감사 기록이 들어 있어'
133
+ + ' 통째로 건드리지 않습니다 — 지우면 /undo 가 되돌릴 것이 없어집니다.'
134
+ + ' 안의 파일 하나가 필요하면 사용자에게 물어보세요.';
135
+ }
115
136
  if (안[0] === 'config.json') {
116
137
  return 'deel 자신의 설정 파일입니다. 게이트웨이 열쇠가 들어 있어 읽지 않습니다.'
117
138
  + ' 연결 상태가 궁금하면 사용자에게 /status 를 쳐 보라고 하세요.';
@@ -6,7 +6,7 @@ import { 무리로돌리기 } from './spawn.js';
6
6
  import { globToRegex, walk, readText, readTextFull, 내부살림 } from './fsutil.js';
7
7
  import { 건너뜀말 } from './ignore.js';
8
8
  import { encode, label as encLabel, decode as decodeBytes, consoleCodepage, looksBinary } from './encoding.js';
9
- import { checkCommand, checkPaths, isMutating } from '../safety/guard.js';
9
+ import { checkCommand, checkPaths, isMutating, 셸이파일에쓰나 } from '../safety/guard.js';
10
10
  import { 띄우기, 나무끊기, 무리끊기, JOBS_TOOL } from './jobs.js';
11
11
  import { 셸명령 } from './shell.js';
12
12
  import { findMatch, applySpans, reindent, TIER_LABELS } from './edit-match.js';
@@ -304,7 +304,25 @@ function 뜰만한낱말(cmd) {
304
304
  * @returns {string[]} 떠 둔 파일들의 보인 이름
305
305
  */
306
306
  function 바꾸기전스냅샷(cmd, ctx) {
307
- if (!isMutating(cmd)) return [];
307
+ /*
308
+ * ── 쓰는 꼴도 뜬다 ────────────────────────────────────────────────────
309
+ *
310
+ * 여태 `isMutating(cmd)` 하나로 갈랐다. 그건 `mv`·`cp`·`rm` 같은 **첫
311
+ * 낱말**만 보는 자다. 그런데 모델이 셸에서 파일을 고치는 흔한 방법은
312
+ * `echo x > f` · `sed -i` · `tee` · `prettier --write` 이고, 그 전부가
313
+ * 여기서 빠져나갔다 — 열한 가지 중 `rm` 하나만 걸렸다.
314
+ *
315
+ * 그냥 못 뜨는 것으로 끝나지 않았다. 스냅샷이 없으면 그 턴은 기록을 한 줄도
316
+ * 안 남기고, 그러면 History.turns() 가 그 턴을 못 본다. /undo 는 그 턴을
317
+ * **뛰어넘어 앞의 무관한 턴**을 되돌리고 초록색 성공 줄을 찍었다. 사용자는
318
+ * 지키고 싶던 것을 잃고 지우고 싶던 것은 그대로 두게 된다.
319
+ *
320
+ * MUTATING 을 넓히면 안 된다 — 승인 창과 이중실행 거부가 같이 물고 있어서
321
+ * `echo` 마다 물어보게 된다. 그래서 셸이파일에쓰나() 를 따로 둔다
322
+ * (safety/guard.js).
323
+ */
324
+ const 셸쓰기 = 셸이파일에쓰나(cmd);
325
+ if (!isMutating(cmd) && !셸쓰기) return [];
308
326
  const 뜬것 = [];
309
327
  for (const t of 뜰만한낱말(cmd)) {
310
328
  if (뜬것.length >= 스냅샷상한) break;
@@ -313,7 +331,19 @@ function 바꾸기전스냅샷(cmd, ctx) {
313
331
  // 뜨는 데 실패했다고 명령 자체를 막으면 안 되는 명령까지 막힌다.
314
332
  try { abs = ctx.scope.resolve(t); } catch { continue; }
315
333
  try {
316
- if (!existsSync(abs) || statSync(abs).isDirectory()) continue;
334
+ const 있나 = existsSync(abs);
335
+ if (있나 && statSync(abs).isDirectory()) continue;
336
+ /*
337
+ * 쓰는 꼴이면 **없는 파일도** 떠 둔다.
338
+ *
339
+ * `echo x > 새파일.js` 는 파일을 만든다. 「없었다」 는 사실 자체가
340
+ * 되돌릴 거리다 — Write 도구가 이미 그렇게 한다(before: null 이면
341
+ * /undo 가 지운다). 여기만 안 그러면 셸로 만든 파일은 영영 안 지워진다.
342
+ *
343
+ * 옮기기·지우기(isMutating)는 반대다. 없는 파일을 떠 두면 `rm *.tmp`
344
+ * 한 번에 이력이 쓰레기로 찬다. 그래서 이 갈래는 쓰는 꼴에만 연다.
345
+ */
346
+ if (!있나 && !셸쓰기) continue;
317
347
  ctx.history.snapshot(abs, 'Bash');
318
348
  뜬것.push(ctx.scope.show(abs));
319
349
  } catch { /* 못 뜨면 그냥 넘어간다. 명령은 돌아야 한다 */ }