deel-local-cli 1.20.8 → 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%2C587%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.8 ⌂ 이 안
304
+ deel 1.20.9 ⌂ 이 안
305
305
  보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
306
306
  ```
307
307
 
@@ -1289,6 +1289,7 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
1289
1289
 
1290
1290
  | 판 | 무엇이 바뀌었나 |
1291
1291
  |---|---|
1292
+ | [1.20.9](docs/ko/releases/1.20.md#1209) | 안 보이는 것이 아니라 다른 것이 보이고 있었습니다 |
1292
1293
  | [1.20.8](docs/ko/releases/1.20.md#1208) | 못 쟀다는 것을 안 된다고 적고 있었습니다 |
1293
1294
  | [1.20.7](docs/ko/releases/1.20.md#1207) | 서버가 말해 준 것과 반대를 배우고 있었습니다 |
1294
1295
  | [1.20.6](docs/ko/releases/1.20.md#1206) | 2차 리뷰가 판을 조용히 잃고 있었습니다 |
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%2C587%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.8 ⌂ 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,7 @@ 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 |
1353
1354
  | [1.20.8](docs/en/releases/1.20.md#1208) | We were writing down "can't" where the truth was "couldn't measure" |
1354
1355
  | [1.20.7](docs/en/releases/1.20.md#1207) | We were learning the opposite of what the server said |
1355
1356
  | [1.20.6](docs/en/releases/1.20.md#1206) | The second review was losing rounds silently |
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.8",
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",
@@ -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.보정잰것,
package/src/commands.js CHANGED
@@ -110,7 +110,15 @@ function 경로처럼보이나(line) {
110
110
  if (첫낱말.includes('/') || 첫낱말.includes('\\')) return true;
111
111
  // `/tmp` 처럼 슬래시가 하나뿐이어도, 실제로 있는 자리면 경로로 본다.
112
112
  if (첫낱말 && !COMMANDS[첫낱말.toLowerCase()]) {
113
- try { if (existsSync(line.slice(1).trim())) return true; } catch { /* 못 보면 아닌 걸로 */ }
113
+ /*
114
+ * **적힌 그대로**도 보고, 앞 슬래시를 뗀 것도 본다.
115
+ *
116
+ * 앞서는 뗀 것만 봤다. 그러면 `/tmp` 가 지금 폴더의 `tmp` 를 찾다 못 찾아,
117
+ * 바로 위 머리글이 적어 둔 그 자리가 한 번도 안 먹었다. 둘 다 보면 여태
118
+ * 되던 것(지금 폴더의 이름)도 그대로 된다.
119
+ */
120
+ const 통째 = line.trim();
121
+ try { if (existsSync(통째) || existsSync(통째.slice(1))) return true; } catch { /* 못 보면 아닌 걸로 */ }
114
122
  }
115
123
  return false;
116
124
  }
@@ -504,7 +512,8 @@ export async function handle(line, session, ctx) {
504
512
  const 끄는말 = ['off', '끔', '꺼', '끄기', 'n', 'no', 'ㄴ'];
505
513
  const 값 = String(arg ?? '').trim().toLowerCase();
506
514
  if (!값) {
507
- const 켜져있나 = cfg.bell !== false;
515
+ // 지금 세션이 참이다. 설정 파일은 '다음에 켤 때' 값일 뿐이다.
516
+ const 켜져있나 = ctx?.종알림 ? ctx.종알림.켬 !== false : cfg.bell !== false;
508
517
  const 상태 = 켜져있나 ? c.hgreen(말('bell.stateOn')) : c.gray(말('bell.stateOff'));
509
518
  say('');
510
519
  say(` ${c.gray(말('bell.now', { 상태 }))}`);
@@ -522,6 +531,9 @@ export async function handle(line, session, ctx) {
522
531
  }
523
532
  cfg.bell = 켜는말.includes(값);
524
533
  설정남기기(cfg);
534
+ // 이 세션에도 바로 먹여야 한다 — 아래 줄이 그렇게 적는다.
535
+ // repl 은 켤 때 읽어 둔 그릇으로 울릴지 말지를 정한다(repl.js 의 알림).
536
+ if (ctx?.종알림) ctx.종알림.켬 = cfg.bell;
525
537
  say('');
526
538
  say(` ${mark.ok} ${말(cfg.bell ? 'bell.turnedOn' : 'bell.turnedOff')}`);
527
539
  // 켤 때는 한 번 울려 준다. "켰다는데 소리가 나나?" 를 그 자리에서 확인하게.
@@ -726,7 +738,9 @@ export async function handle(line, session, ctx) {
726
738
  */
727
739
  if (/^(auto|자동)(\s|$)/.test(말)) {
728
740
  const 값 = 말.replace(/^(auto|자동)\s*/, '').trim().toLowerCase();
729
- const 켤까 = === '' ? session.autoThink === false : /^(on|켜|켜기|true)$/.test(값);
741
+ // `켬` 켜는 말이다. /bell 받는데 여기만 빠져 있어서, `/think auto 켬`
742
+ // 이 **끄는** 명령이 됐다 — 켜려고 친 말로 꺼지고 화면은 「껐습니다」 였다.
743
+ const 켤까 = 값 === '' ? session.autoThink === false : /^(on|켬|켜|켜기|true|y|yes|ㅇ)$/.test(값);
730
744
  session.autoThink = 켤까;
731
745
  say(` ${mark.ok} ${옮긴말(켤까 ? 'think.autoOn' : 'think.autoOff')}`);
732
746
  showThink(session);
@@ -745,6 +759,12 @@ export async function handle(line, session, ctx) {
745
759
  }
746
760
 
747
761
  if (!THINK_LEVELS.includes(말)) {
762
+ // 못 알아들은 값은 못 알아들었다고 먼저 말한다. 안 그러면 `/think hgih`
763
+ // 가 오류 없이 표만 띄워서, 강도를 올린 줄 알고 그대로 쓰게 된다.
764
+ if (말) {
765
+ say('');
766
+ say(` ${mark.no} ${c.gray(옮긴말('think.unknown', { 값: 말 }))}`);
767
+ }
748
768
  showThink(session);
749
769
  return { handled: true };
750
770
  }
@@ -769,7 +789,16 @@ export async function handle(line, session, ctx) {
769
789
  */
770
790
  case 'mode': {
771
791
  const { 승인, 차례: 승인차례, 표시: 승인표시 } = await import('./ui/approve.js');
772
- if (!MODES[arg]) {
792
+ /*
793
+ * 표에 **제 것으로 있는** 이름만 받는다.
794
+ *
795
+ * `MODES[arg]` 는 물려받은 열쇠에도 참이다. `/mode constructor` 를
796
+ * 치면 목록으로 안 빠지고 그대로 session.mode 에 들어가는데, 그 값은
797
+ * strict 도 confirm 도 아니라 **아무것도 안 물어보는** 상태가 된다
798
+ * (agent/loop.js 의 needsOk). 화면에는 `엄격 → undefined` 가 찍힌다.
799
+ * `--work` 오타가 가장 센 모드로 돌던 것과 같은 자리다.
800
+ */
801
+ if (!Object.hasOwn(MODES, arg)) {
773
802
  rule('승인 방식 — 무엇을 물어볼까', 70);
774
803
  for (const k of 승인차례) {
775
804
  const 지금 = k === session.mode;
@@ -820,7 +849,23 @@ export async function handle(line, session, ctx) {
820
849
  * 되돌린 것이 다음 순간 되살아난다.
821
850
  */
822
851
  case 'undo': {
823
- const n = Math.max(1, parseInt(arg, 10) || 1);
852
+ /*
853
+ * 못 읽는 인자로 **말없이 한 턴을 되돌리지 않는다.**
854
+ *
855
+ * parseInt 는 `다섯`·`abc` 를 NaN, `-2` 를 음수로 준다. 앞서는 둘 다
856
+ * 1 로 깎아서, 다섯 턴을 되돌리려던 사람이 한 턴만 되돌아간 화면을
857
+ * 봤다. 되돌리기는 **파일을 실제로 되돌리는** 명령이라 잘못 읽은 수로
858
+ * 도는 값이 다른 명령보다 비싸다. /work 가 오타를 먼저 말하는 것과
859
+ * 같은 잣대다.
860
+ */
861
+ const 준말 = String(arg ?? '').trim();
862
+ if (준말 && !/^[0-9]+$/.test(준말)) {
863
+ say(` ${mark.no} ${c.gray('몇 턴을 되돌릴지 숫자로 적어 주세요.')} ${c.cyan('/undo 3')}`);
864
+ say(` ${c.gray('그냥')} ${c.cyan('/undo')} ${c.gray('만 치면 한 턴을 되돌립니다.')}`);
865
+ say('');
866
+ return { handled: true };
867
+ }
868
+ const n = Math.max(1, parseInt(준말, 10) || 1);
824
869
  const r = ctx.history.undo(n);
825
870
  /*
826
871
  * 세는 수는 **진짜로 되돌아간 것**이다.
@@ -832,8 +877,17 @@ export async function handle(line, session, ctx) {
832
877
  */
833
878
  const 되돌린수 = r.되돌린수 ?? r.restored.length;
834
879
  ctx.audit.undo({ turns: r.turns, files: 되돌린수 });
835
- if (!r.restored.length) {
880
+ /*
881
+ * **하나도 안 되돌아갔으면** 되돌렸다고 하지 않는다.
882
+ *
883
+ * restored 에는 못 되돌린 것과 손대지 않은 것(바이너리처럼 내용을 못
884
+ * 떠 둔 파일)도 들어 있다. 그 길이만 보면, 되돌아간 파일이 0개인
885
+ * 판에도 `✓ … 파일 0개를 되돌렸습니다` 를 찍고 그 밑에 못 되돌린
886
+ * 파일 목록을 성공 목록처럼 늘어놓았다.
887
+ */
888
+ if (!되돌린수) {
836
889
  say(` ${c.gray(말('undo.nothing'))}`);
890
+ if (r.못한것?.length) say(` ${mark.warn} ${말('undo.failed', { n: r.못한것.length })}`);
837
891
  say('');
838
892
  return { handled: true };
839
893
  }
@@ -1238,6 +1292,33 @@ export async function handle(line, session, ctx) {
1238
1292
  return { handled: true };
1239
1293
  }
1240
1294
 
1295
+ /*
1296
+ * 지우려다 **적고 마는** 자리를 막는다.
1297
+ *
1298
+ * `/memory 지우기` 처럼 번호를 안 붙이면 위 무늬에 안 걸리고 아래
1299
+ * 「이걸 기억해라」 로 떨어졌다. 그래서 `지우기` 라는 낱말이 기억 파일에
1300
+ * **영영 적히고**, 화면에는 「기억했습니다」 라고 떴다. 그 줄은 그 뒤로
1301
+ * 매 요청마다 모델에게 같이 나간다. 지우려던 사람이 쓰레기를 하나 더
1302
+ * 심는 꼴이고, 그것도 성공 표시를 보면서 그렇게 된다.
1303
+ */
1304
+ // 낱말 끝은 `\\b` 로 못 잡는다 — 한글은 \\w 가 아니라 `지우기` 뒤에 낱말
1305
+ // 경계가 안 생긴다. 실제로 이 무늬로 고쳤다가 한글만 그대로 새 나갔다.
1306
+ if (/^(지우기|잊어|forget|rm)(\s|$)/i.test(말)) {
1307
+ const 있는것 = M.읽기(session.root).줄들;
1308
+ say(` ${mark.no} ${c.gray('몇 번째 줄을 지울지 번호를 적어 주세요.')}`);
1309
+ if (있는것.length) {
1310
+ for (const [i, l] of 있는것.entries()) {
1311
+ say(` ${c.gray(String(i + 1).padStart(2))} ${c.white(clip(l, 82))}`);
1312
+ }
1313
+ say('');
1314
+ say(` ${c.cyan('/memory 지우기 1')} ${c.gray('한 줄만')} ${c.cyan('/memory 비우기')} ${c.gray('전부')}`);
1315
+ } else {
1316
+ say(` ${c.gray('지금은 기억해 둔 것이 없습니다.')}`);
1317
+ }
1318
+ say('');
1319
+ return { handled: true };
1320
+ }
1321
+
1241
1322
  // 그 밖의 말은 '이걸 기억해라' 로 본다. 모델을 안 거치고 바로 적는 길이다.
1242
1323
  if (말) {
1243
1324
  const r = M.더하기(session.root, 말);
@@ -1745,12 +1826,23 @@ function showSkills(session, arg) {
1745
1826
  }
1746
1827
  if (q.startsWith('on ')) {
1747
1828
  const term = q.slice(3).trim().toLowerCase();
1748
- let n = 0;
1749
- for (const s of all) {
1750
- s.enabled = 낮게(s.name).includes(term) || 낮게(s.description).includes(term);
1751
- if (s.enabled) n++;
1829
+ const 걸린것 = all.filter((s) => 낮게(s.name).includes(term) || 낮게(s.description).includes(term));
1830
+ /*
1831
+ * 개도 걸리면 **아무것도 안 건드린다.**
1832
+ *
1833
+ * 앞서는 걸림 여부를 그대로 enabled 에 넣었다. 그래서 오타 하나면 전부
1834
+ * false 가 되고, 화면에는 `✓ "리뷰" 에 걸리는 0개만 올립니다` 가 떴다.
1835
+ * 올리려고 친 명령이 가지고 있던 것까지 다 내려 버리는데, 표시는 ✓ 다.
1836
+ * 그다음 모델이 스킬을 못 쓰는 것을 보고 사람은 스킬이 깨진 줄 안다.
1837
+ */
1838
+ if (!걸린것.length) {
1839
+ say(` ${mark.no} "${term}" 에 걸리는 스킬이 없습니다 ${c.gray('— 올라간 것은 그대로 둡니다.')}`);
1840
+ say(` ${c.gray('무엇이 있는지 보려면')} ${c.cyan('/skills')}`);
1841
+ say('');
1842
+ return;
1752
1843
  }
1753
- say(` ${mark.ok} "${term}" 걸리는 ${n}개만 올립니다.`);
1844
+ for (const s of all) s.enabled = 걸린것.includes(s);
1845
+ say(` ${mark.ok} "${term}" 에 걸리는 ${걸린것.length}개만 올립니다.`);
1754
1846
  say('');
1755
1847
  return;
1756
1848
  }
@@ -2295,9 +2387,18 @@ async function 리뷰명령(session, ctx) {
2295
2387
 
2296
2388
  async function 커밋명령(session, ctx, arg = '') {
2297
2389
  const 말한것 = String(arg ?? '').trim();
2298
- const 전부 = /^(전부|all)$/i.test(말한것);
2299
- const 미리보기 = /^(미리보기|preview|dry|--dry-run)$/i.test(말한것);
2300
- const 준제목 = 전부 || 미리보기 ? null : (말한것 || null);
2390
+ /*
2391
+ * `전부` 뒤에 제목을 같이 적을 수 있다.
2392
+ *
2393
+ * 앞서는 `^(전부|all)$` 라 `/commit 전부 버그 수정` 이 **둘 다** 어긋났다 —
2394
+ * 전부가 거짓이 되어 폴더째 바뀐 파일이 빠지고, `전부 버그 수정` 이 통째로
2395
+ * 커밋 제목이 됐다. 어느 쪽도 화면에 안 적힌다.
2396
+ */
2397
+ const 전부친것 = /^(전부|all)(\s+|$)/i.exec(말한것);
2398
+ const 전부 = !!전부친것;
2399
+ const 남은말 = 전부 ? 말한것.slice(전부친것[0].length).trim() : 말한것;
2400
+ const 미리보기 = /^(미리보기|preview|dry|--dry-run)$/i.test(남은말);
2401
+ const 준제목 = 미리보기 ? null : (남은말 || null);
2301
2402
 
2302
2403
  say('');
2303
2404
  rule('커밋', 70);
@@ -2532,7 +2633,21 @@ function 배움명령(session, ctx, arg = '') {
2532
2633
  * 몇 주 쌓은 명령 겪음까지 같이 날아갔다 — 그게 아까워서 사람은 안 비우고,
2533
2634
  * 안 비우니 꺼진 기능을 그냥 안고 쓴다. 전선만 지우면 잃을 것이 없다.
2534
2635
  */
2535
- if (/^(전선|wire)\s*(지우기|clear|forget|비우기)?$/i.test(String(arg).trim())) {
2636
+ /*
2637
+ * 지우는 말은 **반드시 적어야** 한다.
2638
+ *
2639
+ * 앞서는 `\s*(…)?` 라 `/learned 전선` 만 쳐도 곧바로 지웠다. 화면이 알려
2640
+ * 주는 명령은 `/learned 전선 지우기` 라 그 짧은 꼴은 어디에도 안 적혀
2641
+ * 있는데, 「전선에서 뭘 배웠나 보자」 는 마음으로 치면 그것이 지우는
2642
+ * 명령이 됐다. 지우는 명령은 지우겠다고 적었을 때만 지운다.
2643
+ */
2644
+ if (/^(전선|wire)$/i.test(String(arg).trim())) {
2645
+ say(` ${c.gray('전선에서 배운 것만 비우려면')} ${c.cyan('/learned 전선 지우기')}`);
2646
+ say(` ${c.gray('쌓인 것을 보려면')} ${c.cyan('/learned')}`);
2647
+ say('');
2648
+ return;
2649
+ }
2650
+ if (/^(전선|wire)\s+(지우기|clear|forget|비우기)$/i.test(String(arg).trim())) {
2536
2651
  배움.지우기('전선');
2537
2652
  if (session.conn) 전선붙이기(session.conn, 배움);
2538
2653
  say(` ${mark.ok} ${c.gray('전선에서 배운 것만 비웠습니다 — 다음 요청부터 짐작으로 다시 시작합니다.')}`);
@@ -2838,7 +2953,7 @@ function 모델급(session, arg = '') {
2838
2953
  return;
2839
2954
  }
2840
2955
 
2841
- if (값 && 별명[]) {
2956
+ if (값 && Object.hasOwn(별명, 값)) {
2842
2957
  session.급정한것 = 별명[값];
2843
2958
  session.급다시재기?.();
2844
2959
  const v = session.급값();
@@ -2849,6 +2964,19 @@ function 모델급(session, arg = '') {
2849
2964
  return;
2850
2965
  }
2851
2966
 
2967
+ /*
2968
+ * 못 알아들은 값은 **못 알아들었다고 말한다.**
2969
+ *
2970
+ * 앞서는 `/grade 크게` 같은 오타가 오류 한 줄 없이 지금 상태 표로
2971
+ * 떨어졌다. 사람은 정해진 줄로 읽고 그대로 쓴다. 바로 옆 `/work` 는
2972
+ * 「그런 모드는 없습니다」 를 먼저 말한다 — 잣대를 같게 맞춘다.
2973
+ */
2974
+ if (값) {
2975
+ say('');
2976
+ say(` ${mark.no} ${c.gray('그런 급은 없습니다:')} ${c.white(값)}`);
2977
+ say(` ${c.gray('고를 수 있는 것:')} ${c.cyan('작음')} ${c.gray('·')} ${c.cyan('보통')} ${c.gray('·')} ${c.cyan('큼')} ${c.gray('·')} ${c.cyan('auto')}`);
2978
+ }
2979
+
2852
2980
  // 인자가 없으면 지금 상태를 보여 준다.
2853
2981
  const g = session.급();
2854
2982
  const v = session.급값();
@@ -3255,8 +3383,17 @@ async function 모델만바꾸기(session, cfg, 모델, ctx = null) {
3255
3383
  // 서버는 그대로지만 문은 같이 지난다. 등록 안 된 서버로 옮겨 가는 길도
3256
3384
  // 여기라서, 여기를 열어 두면 자물쇠에 구멍이 하나 남는다.
3257
3385
  if (!await 나가도되나묻기(session, p)) return;
3258
- if (!이미) 설정남기기(upsert(cfg, p));
3259
- else { cfg.active = p.id; 설정남기기(cfg); }
3386
+ /*
3387
+ * 새로 만든 프로필도 **지금 쓰는 것**으로 못 박는다.
3388
+ *
3389
+ * upsert 는 active 가 비어 있을 때만 채운다. 여기서는 이미 차 있으므로,
3390
+ * 새 프로필을 넣기만 하고 active 는 옛 모델에 그대로 남았다 — 화면에는
3391
+ * 「모델을 X 로 바꿨습니다」 라고 적히고, 다음에 켜면 옛 모델로 돌아온다.
3392
+ * 이미 있는 프로필로 갈아탈 때(아래 갈래)는 제대로 하고 있었다.
3393
+ */
3394
+ if (!이미) upsert(cfg, p);
3395
+ cfg.active = p.id;
3396
+ 설정남기기(cfg);
3260
3397
  연결적용(session, p, ctx);
3261
3398
  say(` ${mark.ok} 모델을 ${c.bold(모델)} 로 바꿨습니다. ${c.gray('서버는 그대로입니다.')}`);
3262
3399
  await 길이맞추기(session, cfg, p);
package/src/i18n/en.js CHANGED
@@ -42,6 +42,7 @@ export const en = {
42
42
  // ── /think screen ──────────────────────────────────────────────────
43
43
  'run.noTools': 'Tool calling is unconfirmed — check with deel diagnose',
44
44
  'think.effort': 'Effort',
45
+ 'think.unknown': 'No such effort level: {값}',
45
46
  'think.profile': 'Profile',
46
47
  'think.detail': 'detail',
47
48
  'think.harder': 'harder',
@@ -98,7 +99,7 @@ export const en = {
98
99
  'cmd.commit.desc': 'commit — stages only what this session changed; message from the diff and the evidence',
99
100
  'cmd.review.desc': 'review the changes in a fresh context — reports findings only, changes nothing',
100
101
  'cmd.paste.desc': 'attach the screenshot sitting in the clipboard (capture, then /paste)',
101
- 'cmd.commit.arg': '[all|preview|title]',
102
+ 'cmd.commit.arg': '[all] [title|preview]',
102
103
  'cmd.sessions.desc': 'past sessions in this folder',
103
104
  'cmd.recall.desc': 'search past sessions — by what was said, not by title',
104
105
  'cmd.recall.arg': '<text>',