deel-local-cli 1.20.7 → 1.20.9

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-9%2C491%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/ko/develop.md)
23
+ [![tests](https://img.shields.io/badge/tests-9%2C762%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)
@@ -301,7 +301,7 @@ deel --offline
301
301
  무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
302
302
 
303
303
  ```
304
- deel 1.20.7 ⌂ 이 안
304
+ deel 1.20.9 ⌂ 이 안
305
305
  보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
306
306
  ```
307
307
 
@@ -1289,6 +1289,8 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
1289
1289
 
1290
1290
  | 판 | 무엇이 바뀌었나 |
1291
1291
  |---|---|
1292
+ | [1.20.9](docs/ko/releases/1.20.md#1209) | 안 보이는 것이 아니라 다른 것이 보이고 있었습니다 |
1293
+ | [1.20.8](docs/ko/releases/1.20.md#1208) | 못 쟀다는 것을 안 된다고 적고 있었습니다 |
1292
1294
  | [1.20.7](docs/ko/releases/1.20.md#1207) | 서버가 말해 준 것과 반대를 배우고 있었습니다 |
1293
1295
  | [1.20.6](docs/ko/releases/1.20.md#1206) | 2차 리뷰가 판을 조용히 잃고 있었습니다 |
1294
1296
  | [1.20.5](docs/ko/releases/1.20.md#1205) | `aws_access_token` 이 안 잡히고 있었습니다 |
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-9%2C491%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/en/develop.md)
23
+ [![tests](https://img.shields.io/badge/tests-9%2C762%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)
@@ -374,7 +374,7 @@ deel --offline
374
374
  The destination is printed at the top of every session:
375
375
 
376
376
  ```
377
- deel 1.20.7 ⌂ inside
377
+ deel 1.20.9 ⌂ inside
378
378
  Sends to this machine 127.0.0.1:11434 ← nowhere else
379
379
  ```
380
380
 
@@ -1350,6 +1350,8 @@ so one run tells you everything.
1350
1350
 
1351
1351
  | Version | What changed |
1352
1352
  |---|---|
1353
+ | [1.20.9](docs/en/releases/1.20.md#1209) | Nothing was missing from the screen — something else was on it |
1354
+ | [1.20.8](docs/en/releases/1.20.md#1208) | We were writing down "can't" where the truth was "couldn't measure" |
1353
1355
  | [1.20.7](docs/en/releases/1.20.md#1207) | We were learning the opposite of what the server said |
1354
1356
  | [1.20.6](docs/en/releases/1.20.md#1206) | The second review was losing rounds silently |
1355
1357
  | [1.20.5](docs/en/releases/1.20.md#1205) | `aws_access_token` was not being caught |
package/bin/deel.js CHANGED
@@ -701,6 +701,33 @@ async function main() {
701
701
 
702
702
  if (flags.help || cmd === 'help') { help(); return 0; }
703
703
 
704
+ /*
705
+ * `--work` 에 오타가 났으면 **여기서 멈춘다.**
706
+ *
707
+ * 모르는 이름은 조용히 기본값(종합)으로 떨어진다. 그런데 종합은 Write·Edit·
708
+ * Bash 를 다 가진 모드다 — `deel --work architcet` 한 번으로, 파일을 안
709
+ * 건드리는 모드로 켠 줄 알고 **고칠 수 있는 상태**로 돌게 된다. 화면에
710
+ * 남는 단서는 상태줄 글자 하나뿐이다.
711
+ *
712
+ * 대화 중에 치는 `/work` 는 관대하게 받아도 된다(틀리면 바로 화면에 뜨고
713
+ * 다시 치면 된다). 깃발은 한 번 켜면 그 세션이 끝까지 그걸로 간다.
714
+ *
715
+ * 판 번호·도움말보다 뒤에 둔다 — 그 둘은 무엇을 잘못 쳤든 답해야 한다.
716
+ */
717
+ if (flags.work !== undefined && flags.work !== true) {
718
+ const { normalize: 모드이름, MODES: 모드들, 보일이름: 모드보임 } = await import('../src/agent/modes.js');
719
+ if (!모드이름(String(flags.work))) {
720
+ const 있는것 = Object.keys(모드들).map((k) => `${모드보임(k)}(${k})`).join(' · ');
721
+ say('');
722
+ say(` 그런 작업 모드가 없습니다: ${String(flags.work)}`);
723
+ say(` 있는 것: ${있는것}`);
724
+ say('');
725
+ say(' 그냥 켜면 「종합」 으로 돕니다 — 그건 파일을 고칠 수 있는 모드입니다.');
726
+ say('');
727
+ return 2;
728
+ }
729
+ }
730
+
704
731
  switch (cmd) {
705
732
  // 한 번만 돌고 끝난다. 스크립트·배치에서 부르는 자리다.
706
733
  //
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deel-local-cli",
3
- "version": "1.20.7",
3
+ "version": "1.20.9",
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",
@@ -105,7 +105,9 @@ export const 캐시일때비울몫 = 0.10;
105
105
  * 늦어질 뿐이고, 있는데 없다고 치면 방금 만든 캐시를 깬다. 그래서 모르는
106
106
  * 쪽은 덜 잃는 쪽으로 기울인다.
107
107
  *
108
- * 넘게 불렀는데도 읽힌 것이 하나도 없으면, 그때는 정말 없는 것이다.
108
+ * **두 번** 불렀는데도 읽힌 것이 하나도 없으면, 그때는 정말 없는 것이다.
109
+ * (여기가 「두 번 넘게」 라고 적혀 있었다. 아래 셈은 두 번째에 이미 단정한다 —
110
+ * 한 번의 차이지만, 적힌 것과 하는 것이 다르면 다음 사람이 그 값을 못 믿는다.)
109
111
  */
110
112
  export const 재볼기회 = 2;
111
113
 
@@ -163,7 +165,37 @@ export function foldImages(session, { keep = KEEP_IMAGES } = {}) {
163
165
  const 자리 = [];
164
166
  ms.forEach((m, i) => { if (그림장수(m)) 자리.push(i); });
165
167
 
166
- const 뺄것 = 자리.slice(0, Math.max(0, 자리.length - keep));
168
+ /*
169
+ * ── 남기는 것은 **장수**다, 메시지 수가 아니다 ─────────────────────────
170
+ *
171
+ * `KEEP_IMAGES` 는 위 머리말대로 「최근 몇 **장**」 이다. 그런데 자리를
172
+ * 메시지 단위로 세고 거기서 keep 을 빼고 있었다. 그래서 한 메시지에 여러
173
+ * 장이 실리면(`@a.png @b.png @c.png` 를 한 줄로 붙이면 그렇게 된다)
174
+ * **남는 장수가 keep 을 훌쩍 넘었다.**
175
+ *
176
+ * 석 장 실린 메시지 + 한 장 실린 메시지 → 자리가 둘이라 한 장도 안 뺀다
177
+ * (남는 것은 넉 장. keep 은 둘)
178
+ *
179
+ * 몇 MB 짜리 사진 셋이 「최근 둘만 남긴다」 는 이름 아래 그대로 남는다.
180
+ *
181
+ * ── 그러면서도 한 덩이는 쪼개지 않는다 ────────────────────────────────
182
+ *
183
+ * 사람이 한 번에 붙인 사진들은 한 이야기다. 그 안에서 어느 장이 더 오래된
184
+ * 것인지 알 길도 없다. 그래서 **마지막 덩이는 몇 장이든 남기고**, 그보다
185
+ * 앞엣것은 장수를 더해 keep 을 안 넘을 때만 남긴다. 마지막 덩이를 빼지
186
+ * 않는 까닭: 방금 보여 준 사진을 그 자리에서 없애면 그 턴이 헛턴이 된다.
187
+ */
188
+ let 남긴장수 = 0;
189
+ let 첫남길자리 = 자리.length;
190
+ for (let k = 자리.length - 1; k >= 0; k--) {
191
+ const 이번장수 = 그림장수(ms[자리[k]]);
192
+ // 마지막 덩이는 무조건 남긴다. 그 뒤로는 넘치면 거기서 끊는다.
193
+ if (k < 자리.length - 1 && 남긴장수 + 이번장수 > keep) break;
194
+ 남긴장수 += 이번장수;
195
+ 첫남길자리 = k;
196
+ }
197
+
198
+ const 뺄것 = 자리.slice(0, 첫남길자리);
167
199
  const 뺀것들 = [];
168
200
  for (const i of 뺄것) {
169
201
  const m = ms[i];
@@ -174,7 +206,14 @@ ${접힘표} 그림 ${장수}장은 자리를 비우려고 뺐습니다. 필요
174
206
  delete ms[i].images;
175
207
  뺀것들.push({ 장수 });
176
208
  }
177
- return { 뺀것: 뺄것.length, 뺀것들 };
209
+ /*
210
+ * `뺀것` 은 **장수**다.
211
+ *
212
+ * 여기가 `뺄것.length` — 곧 **메시지 수**였다. 화면은 이 값을 「그림 N장을
213
+ * 뺐습니다」 로 적으므로, 한 메시지에서 넉 장을 뺀 판에 「1장」 이라고
214
+ * 떴다. 사람이 세어 볼 수 있는 숫자라 틀리면 바로 거짓이 된다.
215
+ */
216
+ return { 뺀것: 뺀것들.reduce((a, x) => a + x.장수, 0), 뺀메시지: 뺄것.length, 뺀것들 };
178
217
  }
179
218
 
180
219
  export function shouldFold(session, at = null) {
@@ -241,8 +280,21 @@ export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN,
241
280
  * 같은 것을 두 번 뽑을 까닭이 없다 — 한 번 뽑아서 같이 들고 간다.
242
281
  */
243
282
  const 아는것 = 이름표.get(첫?.id) ?? null;
283
+ /*
284
+ * 이름은 **한 메시지에 실린 것 전부**를 들고 간다.
285
+ *
286
+ * 아래 「마지막 할 일 목록은 안 접는다」 가 이 이름을 본다. 그런데
287
+ * 첫 것만 들고 가면, 모델이 한 턴에 `Read` 와 `TodoWrite` 를 같이 부른
288
+ * 판에서 — Anthropic 꼴은 그 결과 둘을 **한 메시지**에 싣는다 —
289
+ * 이름이 'Read' 로 잡혀 할 일 목록이 지킴 목록에서 빠진다. 그러면
290
+ * 그 자리가 접히면서 남은 항목이 어디에도 없어진다.
291
+ *
292
+ * 아래 머리말이 「접는 순간 남은 항목이 어디에도 없어진다」 고 적어 둔
293
+ * 바로 그 일이, 도구를 둘 같이 부른 판에서만 조용히 일어났다 (35차 리뷰).
294
+ */
295
+ const 이름들 = 것들.map((x) => 이름표.get(x?.id)?.name ?? x?.name ?? '').filter(Boolean);
244
296
  자리.push({
245
- i, 글,
297
+ i, 글, 이름들,
246
298
  이름: 아는것?.name ?? 첫?.name ?? '',
247
299
  args: 아는것?.args ?? {},
248
300
  });
@@ -262,7 +314,7 @@ export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN,
262
314
  * 남겨 둘수록 해롭다. 지키는 것은 **마지막 하나**뿐이고, 목록 하나는
263
315
  * 몇백 글자라 자리도 거의 안 먹는다.
264
316
  */
265
- const 지킬할일 = 자리.filter((x) => x.이름 === 'TodoWrite').at(-1)?.i ?? -1;
317
+ const 지킬할일 = 자리.filter((x) => x.이름들.includes('TodoWrite')).at(-1)?.i ?? -1;
266
318
 
267
319
  const 접을것 = 자리
268
320
  .slice(0, Math.max(0, 자리.length - keep))
@@ -296,7 +348,31 @@ export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN,
296
348
  * 100자도 안 된다). 그래서 이 수는 언제나 **새로 접을 것**의 수다.
297
349
  */
298
350
  const 예상이득 = 접을것.reduce((a, x) => a + estimateTokens(x.글), 0);
299
- if (예상이득 < 넘을것 || 접을것.length < keep) {
351
+ /*
352
+ * ── 개수를 기다리는 것이 손해가 되는 판 ────────────────────────────────
353
+ *
354
+ * 위 개수 문턱은 「3천 토큰 아끼려고 앞머리를 깨지 말라」 는 말이다. 맞는
355
+ * 말인데, **아낄 것이 3만 토큰일 때도** 똑같이 기다렸다. 큰 파일 읽기 셋이
356
+ * 쌓인 판에서 접기를 미루면 그 다음 안전망은 80% 요약 압축이고, 그쪽은
357
+ * 대화를 **통째로 요약으로 갈아 치우는** 훨씬 비싼 일이다. 개수를 기다리다
358
+ * 더 큰 것을 치르는 셈이다.
359
+ *
360
+ * 그래서 기다림을 **값으로** 판단한다. 앞머리를 깨는 값은 **접은 뒤에**
361
+ * 그 자리부터 다시 보내야 하는 토큰이다 — 캐시에서 싸게 읽던 것을 정가로
362
+ * 다시 쓴다. 아끼는 것이 그보다 크면 지금 접는 것이 이득이라, 개수를 더
363
+ * 기다리지 않는다.
364
+ *
365
+ * 접기 전 양에서 아끼는 양을 뺀다. 접을 글 자체를 대가로 세면 안 된다 —
366
+ * 그건 접은 뒤에 사라지는 것이라, 세는 순간 늘 「기다려라」 가 나온다.
367
+ */
368
+ const 깰자리 = 접을것[0]?.i ?? -1;
369
+ const 깨는값 = 깰자리 < 0 ? Infinity
370
+ : Math.max(0, ms.slice(깰자리).reduce((a, m) => a + estimateTokens(
371
+ 도구결과인가(m) ? 결과들(m).map((x) => x.글).join('\n')
372
+ : (typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '')),
373
+ ), 0) - 예상이득);
374
+ const 기다릴까 = 접을것.length < keep && 예상이득 <= 깨는값;
375
+ if (예상이득 < 넘을것 || 기다릴까) {
300
376
  return {
301
377
  접은것: 0, 아낀토큰: 0, 접은것들: [], 미룸: true,
302
378
  모인것: 예상이득, 모인개수: 접을것.length, 넘을것,
@@ -352,7 +428,18 @@ export function foldToolResults(session, { keep = KEEP_RECENT, min = FOLD_MIN,
352
428
  */
353
429
  return {
354
430
  접은것: 접을것.length, 아낀토큰: Math.max(0, 아낀토큰), 접은것들,
355
- 캐시다시씀: 캐시가도나(session),
431
+ /*
432
+ * 화면에 「캐시를 다시 씁니다」 를 적을까.
433
+ *
434
+ * `캐시가도나()` 를 그대로 쓰고 있었다. 그 함수는 **결정**을 위해 모르는
435
+ * 쪽을 「있다」 로 기울인다(그 머리말) — 그래서 첫 턴에는, 캐시가 아예
436
+ * 없는 창구에서도 늘 참이었다. 그러면 화면은 「값과 지연이 한 번 튑니다」
437
+ * 라고 겁을 주는데 잃을 캐시가 애초에 없다.
438
+ *
439
+ * 결정은 기울여도 되지만 **화면은 아는 것만 적는다.** 캐시를 정말 읽어
440
+ * 본 적이 있을 때만 말한다 (35차 리뷰).
441
+ */
442
+ 캐시다시씀: (session?.usage?.cacheRead ?? 0) > 0,
356
443
  };
357
444
  }
358
445
 
@@ -525,7 +612,15 @@ export async function compact(session, { auto = false, signal = null, onBackoff
525
612
  * 「앞에서 읽은 그대로입니다」 를 내밀면 모델은 대화에 없는 글을 가리키는
526
613
  * 쪽지만 받는다. 통째로 다시 싣는 것보다 훨씬 나쁘다.
527
614
  */
528
- if (folded > 0) session.파일기억?.잊기();
615
+ /*
616
+ * 읽은 파일 수도 같이 내린다.
617
+ *
618
+ * 아래 성공한 길은 `filesRead.clear()` 도 하는데 이쪽은 안 했다. 그 표는
619
+ * `/context` 가 「도구 결과 N개」 로 세는 자리라(agent/session.js), 물러선
620
+ * 뒤에는 **대화에 없는 파일까지 세어 보여 줬다.** 사람이 자리를 왜 이만큼
621
+ * 쓰는지 그 화면으로 판단하는데, 거기 없는 것이 섞여 있으면 안 된다.
622
+ */
623
+ if (folded > 0) { session.파일기억?.잊기(); session.filesRead?.clear?.(); }
529
624
  const after = session.breakdown().used;
530
625
  return {
531
626
  ok: folded > 0, folded, before, after, fallback: true,
@@ -298,10 +298,27 @@ export class 배움 {
298
298
  return out.length > 1 ? out.join('\n') : null;
299
299
  }
300
300
 
301
- /** 사람이 지우라고 한 것. 파일째 비운다. */
301
+ /**
302
+ * 사람이 지우라고 한 것. 파일째 비운다.
303
+ *
304
+ * ── `'전선'` 은 왜 따로 있나 ───────────────────────────────────────────
305
+ *
306
+ * 전선 모양은 **틀리게 배울 수 있는 유일한 것**이다 (backend/wire.js 의
307
+ * 배울전선: "잘못 배우는 것은 못 배우는 것보다 나쁘다"). 게이트웨이가
308
+ * 한동안 잘못 답했거나 우리가 문구를 잘못 읽으면, 멀쩡한 기능이 꺼진 채
309
+ * 디스크에 굳는다 — 그리고 그 창구에서는 영영 그대로다.
310
+ *
311
+ * 그런데 되돌릴 길이 **「쌓아 둔 것 전부 비우기」 하나**였다. 그러면 몇 주
312
+ * 걸려 쌓은 명령 겪음과 토큰 보정까지 같이 날아간다. 그 값이 아까워서
313
+ * 사람은 안 비우고, 안 비우니 꺼진 기능을 그냥 안고 쓴다.
314
+ *
315
+ * 그래서 전선만 지운다. 다음 요청에서 짐작으로 다시 시작하고, 서버가
316
+ * 여전히 거절하면 같은 것을 다시 배운다 — 잃을 것이 없다.
317
+ */
302
318
  지우기(어디 = '전부') {
303
319
  if (어디 === '전부' || 어디 === '폴더') { this.폴더 = 빈것(); this.#저장폴더(); }
304
320
  if (어디 === '전부' || 어디 === '모델') { this.집 = 빈것(); this.#저장집(); }
321
+ if (어디 === '전선') { this.집.전선 = {}; this.#저장집(); }
305
322
  return this;
306
323
  }
307
324
 
@@ -36,6 +36,9 @@ import { load, activeProfile, resolveKey } from '../config.js';
36
36
  import { isLocalHost, isOffline } from '../safety/network.js';
37
37
  import { 잠잠기본, 무소식기본, 연결기본 } from '../backend/http.js';
38
38
  import { 인증서설정 } from '../backend/clientcert.js';
39
+ import { 받기설정 } from '../safety/authcmd.js';
40
+ // 열쇠받기 명령을 정책이 못박아 뒀을 수 있다 — 받기설정 이 그 값을 같이 본다.
41
+ import { 정책읽기 } from '../safety/policy.js';
39
42
 
40
43
  // 컨텍스트를 못 알아냈을 때 쓰는 값. repl.js 와 같은 값을 봐야 한다.
41
44
  export const CTX_DEFAULT = 32768;
@@ -67,6 +70,25 @@ export function 연결만들기(prof, { ctx = null, maxTokens = null } = {}) {
67
70
  tools: prof.tools ?? false,
68
71
  json: prof.json ?? false,
69
72
  think: prof.think ?? false,
73
+ /*
74
+ * ── 이 두 줄을 빼면 회사 프로필로는 물어볼 수가 없다 ────────────────
75
+ *
76
+ * conn 을 짓는 자리는 다섯이다 — repl.js · oneshot.js · acp/serve.js ·
77
+ * commands.js 의 연결적용, 그리고 여기. 앞의 넷은 이 둘을 넣는데 여기만
78
+ * 빠져 있었다. 이 함수를 쓰는 자리가 둘인데 **둘 다 남에게 넘기는
79
+ * 자리**다 — /consult 로 한 번 물어보기, Task 로 하위에 떼어 주기.
80
+ *
81
+ * 열쇠를 명령으로 받아 오는 프로필(SSO 토큰)은 prof.key 가 비어 있다.
82
+ * 그래서 열쇠받기 없이 지으면 Authorization 이 안 실려 401 이 나고,
83
+ * 열쇠를 다시 받는 길도 잠긴다(adapter.js 의 열쇠다시받을까 는
84
+ * conn.열쇠받기 를 본다). `/model 회사창구` 는 되는데 `/consult
85
+ * 회사창구` 만 안 되는, 까닭이 화면에 안 적히는 꼴이 된다.
86
+ *
87
+ * vision 도 같다 — 안 넣으면 그림을 볼 수 있는 모델에 하위 작업을
88
+ * 떼어 주면서 그림을 못 보는 것으로 친다.
89
+ */
90
+ 열쇠받기: 받기설정(prof, { 정책값: 정책읽기().값 }),
91
+ vision: prof.vision ?? false,
70
92
  };
71
93
  }
72
94
 
@@ -115,7 +115,7 @@ export const MODES = {
115
115
  id: 'auto',
116
116
  hintEn: "picks the right mode for what you ask",
117
117
  sayEn: "You are in **Auto** mode. What comes next is not fixed.\n\n- Work out what kind of job this is first, then do it that way.\n If it is an edit, read before editing. If it is a diagnosis, confirm before concluding.\n If it is an explanation, back it with the files.\n- For a large job, break it into steps with TodoWrite and **finish everything you wrote down.**\n Do not write the list and then ask. If there are several chunks, hand them off with Task —\n a subtask runs in its own window, so yours does not fill up.\n- For code you did not write, get the shape with Outline. Use Grep to gather every place it\n touches, then Read **only around the lines you found** (offset/limit). Do not read whole\n files - pour it all in at once and the earlier turns fold away, and you answer having\n forgotten them.\n- Fire unrelated reads **together in one message.** Read tools run side by side; one call at\n a time adds that many round trips, and that is the waiting the person feels.\n- Making several files: one Write call (files array). Do not call it once per file.\n Several places to edit: one Edit call the same way (edits array).\n- Commands that never end (dev servers, watch) need background: true on Bash. Called plainly\n they die on timeout. After starting one, read its output with Jobs, and end it with Jobs when done.\n- Verify what can be verified — call Verify. Do not call something done that you did not check.\n- Do what the job needs. Do not start work the job did not ask for.",
118
- say짧게En: "**Auto** mode. What comes next is not fixed.\n- Work out what kind of job this is, then do it that way.\n- Large job: break it up with TodoWrite and **finish it all.** Several chunks: hand off with Task.\n- Code you did not write: Outline -> Grep -> Read **only around what you found** (offset/limit).\n- Fire unrelated reads together in one message - they run side by side.\n- Several files: one Write (files array). Several edits: one Edit (edits array).\n- Commands that never end (dev server, watch): background: true on Bash. Read with Jobs, end with Jobs.\n- Verify before you finish. Do not call something done that you did not check.",
118
+ say짧게En: "**Auto** mode. What comes next is not fixed.\n- Work out what kind of job this is, then do it that way.\n- Large job: break it up with TodoWrite and **finish it all.** Several chunks: hand off with Task.\n- Code you did not write: Outline -> Grep -> Read **only around what you found** (offset/limit).\n- Fire unrelated reads together in one message - they run side by side.\n- Several files: one Write (files array). Several edits: one Edit (edits array).\n- Commands that never end (dev server, watch): background: true on Bash. Read with Jobs, end with Jobs.\n- Verify before you finish. Do not call something done that you did not check.\n- Do what the job needs. Do not start work the job did not ask for.",
119
119
  name: '종합',
120
120
  en: 'Auto',
121
121
  glyph: '◎',
@@ -132,6 +132,9 @@ export const MODES = {
132
132
  '- 파일 여러 개는 Write 한 번에 (files 배열), 고칠 자리 여럿은 Edit 한 번에 (edits 배열).',
133
133
  '- 끝내지 않는 명령(dev 서버·watch)은 Bash 에 background: true. Jobs 로 읽고 끝낸다.',
134
134
  '- 끝내기 전에 Verify. 확인 못 한 것을 됐다고 하지 마라.',
135
+ // 「빠진 규칙은 없다」 는 위 주석대로 되려면 이 줄이 있어야 한다.
136
+ // 시키지 않은 일을 벌이는 것은 작은 창에서 더 비싸다.
137
+ '- 시키는 일을 해라. 시키지 않은 일을 벌이지 마라.',
135
138
  ].join('\n'),
136
139
  say: [
137
140
  '지금은 **종합** 모드다. 무슨 일이 올지 정해져 있지 않다.',
@@ -206,6 +209,7 @@ export const MODES = {
206
209
  id: 'architect',
207
210
  hintEn: "shapes the structure · touches no files",
208
211
  sayEn: "This is **design**. You have not been given the tools that change files.\n\nRead first. Designing without knowing the current structure is imagining, not designing.\n - Start with Outline for the shape of the folder. Narrow with Glob/Grep, then Read\n **only around the lines you found** (offset/limit). Read whole files and the earlier\n ones fold away after a handful - then you have not seen the structure, only the last file\n - Fire unrelated reads together in one message - they run side by side\n - Work out what depends on what, and in which direction\n\nThen answer in this order.\n 1. Current structure — files, their roles, where the boundaries are (point with path:line)\n 2. What is wrong — why the current shape does not hold\n 3. Two or three options — what each gains, what each costs, how much work it is\n 4. One recommendation with the reason, and why you dropped the others\n 5. The files this affects\n\nFind the answer inside the conventions this code already uses. Bringing in a new framework is\nthe last resort, and if you go there, first say why the existing conventions cannot do it.",
212
+ say짧게En: "**Design** mode. You have no tools that change files.\n- Read first — designing without the current structure is imagining, not designing.\n Outline -> Glob/Grep -> Read **only around what you found** (offset/limit). Never whole files.\n Fire unrelated reads together in one message. Work out what depends on what, and which way.\n- Answer in this order: 1) current structure (path:line) 2) what is wrong 3) two or three options\n (what each gains, costs, how much work) 4) one recommendation with the reason, and why you dropped\n the others 5) the files this affects.\n- Find it inside the conventions this code already uses. A new framework is the last resort — if you\n go there, first say why the existing conventions cannot do it.",
209
213
  name: '설계',
210
214
  en: 'Architect',
211
215
  glyph: '◈',
@@ -213,6 +217,16 @@ export const MODES = {
213
217
  tools: [...읽기, ...계획],
214
218
  effort: 'deep',
215
219
  think: 'high',
220
+ say짧게: [
221
+ '**설계** 모드다. 파일을 바꾸는 도구는 없다.',
222
+ '- 먼저 읽어라 — 구조를 모르면 설계가 아니라 상상이다.',
223
+ ' Outline → Glob/Grep → **짚은 자리 앞뒤만** Read (offset·limit). 통째로 읽지 마라.',
224
+ ' 상관없는 읽기는 한 번에 같이 부른다. 무엇이 무엇에 기대는지(의존 방향)를 본다.',
225
+ '- 이 차례로 답한다: 1) 지금 구조(경로:줄) 2) 무엇이 문제인가 3) 선택지 2~3개(얻는 것·잃는',
226
+ ' 것·드는 손) 4) 추천 하나와 버린 이유 5) 영향받는 파일.',
227
+ '- 이 코드가 이미 쓰는 관례 안에서 찾아라. 새 틀은 마지막 수단이고, 그때는 기존 관례로',
228
+ ' 왜 안 되는지 먼저 밝혀라.',
229
+ ].join('\n'),
216
230
  say: [
217
231
  "지금 하는 일은 **설계**다. 파일을 바꾸는 도구는 주어지지 않았다.",
218
232
  "",
@@ -238,7 +252,11 @@ export const MODES = {
238
252
  ask: {
239
253
  id: 'ask',
240
254
  hintEn: "explains only · changes nothing",
241
- sayEn: "This is **explanation**. You change nothing.\n\n- Back it with the files. Give the path and line number (src/a.js:42).\n- If you do not know, say so. Do not invent a plausible answer.\n- Keep it short. Answer what was asked.\n- Do not propose fixes unless asked to fix something. Do not start work you were not asked for.",
255
+ // 영어 쪽이 Keep it short 남아 있었다. 한국어는 때문에 여러
256
+ // 가지를 물으면 몇 가지가 빠지던 것을 고쳐 「물음의 크기에 맞춰라」 로
257
+ // 바꿨는데, 영어는 안 바꿔서 **영어로 쓰는 사람에게만 옛 결함이 남아
258
+ // 있었다.** 읽는 절차도 빠져 있어 읽어야 나오는 답을 짐작으로 냈다.
259
+ sayEn: "This is **explanation**. You change nothing.\n\n- Back it with the files. Give the path and line number (src/a.js:42).\n- If you do not know, say so. Do not invent a plausible answer.\n- **Match the size of the answer to the size of the question.** One line asked, one line back;\n several things asked, every one of them answered. Short is not the goal.\n- If it takes reading to answer, read — Outline -> Grep -> Read **only around what you found**.\n Fire unrelated reads together in one message.\n- Do not propose fixes unless asked to fix something. Do not start work you were not asked for.",
242
260
  name: '묻기',
243
261
  en: 'Ask',
244
262
  glyph: '◇',
@@ -364,6 +382,7 @@ export const MODES = {
364
382
  id: 'debug',
365
383
  hintEn: "finds the cause",
366
384
  sayEn: "This is **finding the cause**. Do not fix by guessing.\n\nFollow this order.\n 1. Restate the symptom in one sentence — what happens when you do what.\n 2. Get a reproduction. If there is none, build one. Without it you cannot tell whether you fixed it.\n 3. Form two or three hypotheses. For each, write down what you should see if it is true.\n 4. Check them one at a time, for real — read the logs, run something small, print the value.\n Only what you checked is fact. What you did not check is still a hypothesis.\n If several hypotheses turn on reading alone, read for all of them in one message.\n 5. When you name the cause, bring the evidence. A cause that starts with \"probably\" is not a cause.\n 6. After fixing, run the reproduction from step 2 again. If it is not fixed, go back to step 3.\n\nDo not change several places at once. You will not know which one fixed it.",
385
+ say짧게En: "**Finding the cause.** Do not fix by guessing.\n1. Restate the symptom in one sentence — what happens when you do what.\n2. Get a reproduction. If there is none, build one — without it you cannot tell whether you fixed it.\n3. Form two or three hypotheses. For each, write what you should see if it is true.\n4. Check them one at a time, for real. Only what you checked is fact.\n If several turn on reading alone, read for all of them in one message.\n5. Name the cause with the evidence. A cause that starts with \"probably\" is not a cause.\n6. After fixing, run the reproduction from step 2 again. Not fixed — back to step 3.\nDo not change several places at once. You will not know which one fixed it.",
367
386
  name: '디버그',
368
387
  en: 'Debug',
369
388
  glyph: '◉',
@@ -371,6 +390,17 @@ export const MODES = {
371
390
  tools: [...읽기, ...계획, ...쓰기, ...확인, ...쪼개기],
372
391
  effort: 'deep',
373
392
  think: 'high',
393
+ say짧게: [
394
+ '**원인 찾기** 모드다. 짐작으로 고치지 마라.',
395
+ '1. 증상을 한 문장으로 다시 적는다 — 무엇을 했을 때 무엇이 일어나는가.',
396
+ '2. 재현 방법을 확보한다. 없으면 만든다 — 재현 못 하면 고쳤는지도 알 수 없다.',
397
+ '3. 가설 2~3개. 각각 「맞다면 무엇이 보여야 하는가」 를 같이 적는다.',
398
+ '4. 하나씩 실제로 확인한다. 확인한 것만 사실이다.',
399
+ ' 읽기로 가릴 가설이 여럿이면 한 번에 같이 읽는다 — 나란히 돈다.',
400
+ '5. 원인에는 증거를 같이 댄다. 「아마」 로 시작하는 원인은 원인이 아니다.',
401
+ '6. 고친 뒤 2번 절차로 다시 확인한다. 안 고쳐졌으면 3번으로 돌아간다.',
402
+ '한 번에 여러 곳을 고치지 마라 — 무엇이 고쳤는지 알 수 없게 된다.',
403
+ ].join('\n'),
374
404
  say: [
375
405
  "지금 하는 일은 **원인 찾기**다. 짐작으로 고치지 마라.",
376
406
  "",
@@ -391,7 +421,8 @@ export const MODES = {
391
421
  plan: {
392
422
  id: 'plan',
393
423
  hintEn: "plan first · run it after approval",
394
- sayEn: "This is **planning**. You have not been given the tools that change files.\nDo not try to edit code. Produce a plan and stop.\n\nConfirm first — a plan built without knowing the current state is a wish, not a plan.\n Start with Outline for the shape, narrow with Glob/Grep, then Read **only around the lines\n you found** (offset/limit). Fire unrelated reads together in one message.\n\nThen write it in this order.\n 1. Goal — what does \"done\" look like (as a sentence you can check)\n 2. Current state — the files involved and what they do now (point with path:line)\n 3. What changes — per file, what and why\n 4. Order — step by step. Each step small enough to check on its own\n 5. Risks — what could break, and how to get back if it does\n 6. How to check — what do you run to know it worked\n\nWrite the steps into TodoWrite as well. After approval you continue straight from them.\n The number of steps is not fixed — match it to the size of the job. Do not force it to three.\n A small job ends in two or three; a large one lists all ten or more.\nIf something is unknown, do not invent it — write \"this needs to be confirmed\".\n\nEnd with \"Shall I go ahead with this?\". Once approved, switch to /code and run it.",
424
+ sayEn: "This is **planning**. You have not been given the tools that change files.\nDo not try to edit code. Produce a plan and stop.\n\nConfirm first — a plan built without knowing the current state is a wish, not a plan.\n Start with Outline for the shape, narrow with Glob/Grep, then Read **only around the lines\n you found** (offset/limit). Fire unrelated reads together in one message.\n\nThen write it in this order.\n 1. Goal — what does \"done\" look like (as a sentence you can check)\n 2. Current state — the files involved and what they do now (point with path:line)\n 3. What changes — per file, what and why\n 4. Order — step by step. Each step small enough to check on its own\n 5. Risks — what could break, and how to get back if it does\n 6. How to check — what do you run to know it worked\n\nWrite the steps into TodoWrite as well. After approval you continue straight from them.\n The number of steps is not fixed — match it to the size of the job. Do not force it to three.\n A small job ends in two or three; a large one lists all ten or more.\nIf something is unknown, do not invent it — write \"this needs to be confirmed\".\n\nEnd with \"Shall I go ahead with this?\".\n You cannot make the edits in this mode — you have no tools for it. Once approved,\n **tell the person to type `/code`** you have no way to switch modes yourself.",
425
+ say짧게En: "**Planning.** You have no tools that change files. Produce a plan and stop.\n- Confirm first — a plan built without the current state is a wish, not a plan.\n Outline -> Glob/Grep -> Read **only around what you found** (offset/limit). Unrelated reads together.\n- Write it in this order: 1) goal (a sentence you can check) 2) current state (path:line)\n 3) what changes, per file, and why 4) order — each step small enough to check on its own\n 5) risks and how to get back 6) how to check it worked.\n- Put the steps in TodoWrite too. Match the number to the size of the job — do not force it to three.\n- If something is unknown, do not invent it — write \"this needs to be confirmed\".\n- End with \"Shall I go ahead with this?\". You have no tools to make the edits, so once approved,\n **tell the person to type `/code`** — you cannot switch modes yourself.",
395
426
  name: '계획',
396
427
  en: 'Plan',
397
428
  glyph: '☰',
@@ -399,6 +430,17 @@ export const MODES = {
399
430
  tools: [...읽기, ...계획],
400
431
  effort: 'deep',
401
432
  think: 'high',
433
+ say짧게: [
434
+ '**계획** 모드다. 파일을 바꾸는 도구는 없다. 계획을 내고 멈춘다.',
435
+ '- 먼저 확인하라 — 지금 상태를 모르면 계획이 아니라 희망이다.',
436
+ ' Outline → Glob/Grep → **짚은 자리 앞뒤만** Read (offset·limit). 상관없는 읽기는 한 번에.',
437
+ '- 이 차례로 적어라: 1) 목표(확인할 수 있는 문장) 2) 지금 상태(경로:줄) 3) 바꿀 것(파일별로',
438
+ ' 무엇을 왜) 4) 순서(각 단계는 따로 확인할 수 있는 크기로) 5) 위험과 되돌리는 길 6) 확인 방법.',
439
+ '- 단계는 TodoWrite 로도 적어라. 수는 일의 크기에 맞춘다 — 세 개로 맞추지 마라.',
440
+ '- 모르는 것은 지어내지 말고 「이건 확인이 필요하다」 고 적어라.',
441
+ '- 「이대로 진행할까요?」 로 끝내라. 고치는 도구가 없으니, 승인을 받으면',
442
+ ' **사람에게 `/code` 를 치라고 적어 줘라** — 네가 모드를 바꿀 길은 없다.',
443
+ ].join('\n'),
402
444
  say: [
403
445
  "지금 하는 일은 **계획 세우기**다. 파일을 바꾸는 도구는 주어지지 않았다.",
404
446
  "코드를 고치려 들지 마라. 계획을 내고 멈춘다.",
@@ -420,7 +462,12 @@ export const MODES = {
420
462
  " 작은 일이면 두세 개로 끝내고, 큰 일이면 열 개가 넘어도 그대로 다 적어라.",
421
463
  "모르는 것이 있으면 지어내지 말고 \"이건 확인이 필요하다\" 고 적어라.",
422
464
  "",
423
- "마지막에 \"이대로 진행할까요?\" 끝내라. 승인을 받으면 /code 바꿔 실행한다.",
465
+ // 모드를 바꾸는 도구는 모델에게 없다. `/code` 사람이 치는 명령이다.
466
+ // 「바꿔 실행한다」 고 적어 두면, 승인을 받은 모델이 고칠 도구도 없이
467
+ // 고치려 들다 아무것도 못 하고 멈춘다 — 사람은 기다리고만 있게 된다.
468
+ "마지막에 \"이대로 진행할까요?\" 로 끝내라.",
469
+ " 실제로 고치는 것은 이 모드에서 안 된다(고치는 도구가 없다). 승인을 받으면",
470
+ " **사람에게 `/code` 를 치라고 적어 줘라** — 네가 모드를 바꿀 길은 없다.",
424
471
  ].join('\n'),
425
472
  },
426
473
 
@@ -473,7 +520,16 @@ export const DEFAULT = 'auto';
473
520
  export function normalize(v) {
474
521
  const s = String(v ?? '').trim().toLowerCase();
475
522
  if (!s) return null;
476
- if (MODES[s]) return s;
523
+ /*
524
+ * 열쇠가 **제 것인지**를 본다.
525
+ *
526
+ * 그냥 `MODES[s]` 로 보면 `constructor`·`__proto__` 같은 이름에 물려받은
527
+ * 것이 잡혀 참이 된다. 그러면 그 이름이 모드로 통과하고, `get()` 이
528
+ * 돌려준 것에는 `tools` 가 없어 **터진다** — 하필 터지는 자리가
529
+ * `canWrite()` 이고, 그것은 하위 작업이 부모보다 셀 수 없게 막는
530
+ * 자리(tools/task.js 의 하위모드)다. 아래 별명 표도 같은 이유로 같이 막는다.
531
+ */
532
+ if (Object.hasOwn(MODES, s)) return s;
477
533
  const 별명 = {
478
534
  '종합': 'auto', '자동': 'auto', '기본': 'auto', 'auto': 'auto', '맡김': 'auto',
479
535
  '코드': 'code', 'c': 'code',
@@ -488,7 +544,7 @@ export function normalize(v) {
488
544
  'inspect': 'inspect', 'audit': 'inspect', 'review': 'inspect', 'i': 'inspect',
489
545
  '총괄': 'orchestrator', '오케': 'orchestrator', 'orch': 'orchestrator', 'o': 'orchestrator',
490
546
  };
491
- return 별명[s] ?? null;
547
+ return Object.hasOwn(별명, s) ? 별명[s] : null;
492
548
  }
493
549
 
494
550
  export function get(id) {
@@ -502,7 +558,11 @@ export function get(id) {
502
558
  * 규칙(session.js)이 줄어드는 자리와 같은 경계다. 셋이 같이 움직여야
503
559
  * '작은 창에서는 고정 몫을 줄인다' 가 흩어진 세 결정이 아니라 한 결정이 된다.
504
560
  *
505
- * 짧은 판이 없는 모드는 원래 짧은 것들이다(묻기 125토큰). 그냥 그대로 쓴다.
561
+ * 짧은 판이 없는 것은 **묻기 하나**다(340자 — 원래 짧다). 그냥 그대로 쓴다.
562
+ *
563
+ * 앞서는 설계·디버그·계획에도 짧은 판이 없었다. 「원래 짧은 것들」 이라고
564
+ * 적어 두었지만 계획은 761자였다 — 작은 창에서 줄이자고 만든 자리가 정작 큰
565
+ * 모드에서 안 줄었다. 그래서 지금은 검사가 그 짝을 지킨다(test/modes.test.js).
506
566
  */
507
567
  export function 말(id, ctx) {
508
568
  const m = get(id);
@@ -899,8 +899,18 @@ export class Session {
899
899
  { label: 옮긴말('ctx.toolResults', { n: this.filesRead.size }), n: files },
900
900
  ];
901
901
  const used = rows.reduce((a, r) => a + r.n, 0);
902
+ /*
903
+ * 창 크기를 **알아낸 것인지**도 같이 내놓는다.
904
+ *
905
+ * 안 알려 주는 창구에서는 32k 로 둔다(그래야 무엇이든 돌아간다). 그런데
906
+ * 화면이 그 값을 잰 것과 똑같은 낯으로 `5.6k/32k · 17%` 라고 적으면,
907
+ * 실제 창이 8k 인 자리에서 답이 잘리는 동안 게이지는 초록이다. 같은
908
+ * 상태줄이 모델 급은 짐작일 때 `◈ 보통?` 으로 흐리게 적는다 — 창 크기도
909
+ * 같은 규칙을 따라야 한다(ui/status.js).
910
+ */
911
+ const 잰것 = this.conn.ctx != null;
902
912
  const total = this.conn.ctx ?? 32768;
903
- return { rows, used, total, left: Math.max(0, total - used) };
913
+ return { rows, used, total, 총잰것: 잰것, left: Math.max(0, total - used) };
904
914
  }
905
915
 
906
916
  /**
@@ -918,6 +928,7 @@ export class Session {
918
928
  rows,
919
929
  used,
920
930
  total: 날것.total,
931
+ 총잰것: 날것.총잰것,
921
932
  left: Math.max(0, 날것.total - used),
922
933
  보정: this.보정,
923
934
  보정잰것: this.보정잰것,