deel-local-cli 1.20.13 → 2.0.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.
Files changed (155) hide show
  1. package/README.ko.md +96 -78
  2. package/README.md +119 -81
  3. package/bin/deel.js +398 -32
  4. package/package.json +2 -1
  5. package/src/acp/jsonrpc.js +87 -7
  6. package/src/acp/map.js +23 -3
  7. package/src/acp/serve.js +333 -25
  8. package/src/agent/agents.js +110 -12
  9. package/src/agent/askcheck.js +50 -6
  10. package/src/agent/asks.js +59 -5
  11. package/src/agent/budget.js +9 -2
  12. package/src/agent/card.js +8 -2
  13. package/src/agent/commit.js +168 -16
  14. package/src/agent/compact.js +93 -21
  15. package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
  16. package/src/agent/effort.js +85 -15
  17. package/src/agent/evidence.js +63 -7
  18. package/src/agent/evolve.js +132 -24
  19. package/src/agent/grade.js +12 -3
  20. package/src/agent/loop.js +408 -81
  21. package/src/agent/memory.js +176 -29
  22. package/src/agent/mention.js +22 -5
  23. package/src/agent/models.js +31 -7
  24. package/src/agent/modes.js +25 -10
  25. package/src/agent/outschema.js +381 -56
  26. package/src/agent/pins.js +33 -2
  27. package/src/agent/project.js +73 -8
  28. package/src/agent/recall.js +40 -9
  29. package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
  30. package/src/agent/review.js +143 -13
  31. package/src/agent/route.js +253 -26
  32. package/src/agent/salvage.js +2 -1
  33. package/src/agent/session.js +457 -31
  34. package/src/agent/store.js +232 -22
  35. package/src/agent/threads.js +52 -3
  36. package/src/backend/adapter.js +508 -85
  37. package/src/backend/azure.js +12 -1
  38. package/src/backend/cachemark.js +31 -5
  39. package/src/backend/clientcert.js +23 -1
  40. package/src/backend/ctxsize.js +56 -9
  41. package/src/backend/detect.js +252 -19
  42. package/src/backend/http.js +383 -42
  43. package/src/backend/learn.js +101 -11
  44. package/src/backend/mcp.js +333 -24
  45. package/src/backend/price.js +61 -13
  46. package/src/backend/probe.js +188 -36
  47. package/src/backend/proxy.js +108 -14
  48. package/src/backend/quota.js +226 -30
  49. package/src/backend/retry.js +46 -3
  50. package/src/backend/scan.js +47 -10
  51. package/src/backend/scanui.js +57 -7
  52. package/src/backend/toolfit.js +204 -22
  53. package/src/backend/vision.js +14 -1
  54. package/src/backend/wire.js +58 -10
  55. package/src/cmdnames.js +16 -0
  56. package/src/commands/common.js +70 -0
  57. package/src/commands/extend.js +300 -0
  58. package/src/commands/model.js +852 -0
  59. package/src/commands/view.js +328 -0
  60. package/src/commands/work.js +823 -0
  61. package/src/commands.js +193 -2082
  62. package/src/completion.js +45 -3
  63. package/src/config.js +452 -39
  64. package/src/configexplain.js +125 -14
  65. package/src/doctor.js +164 -26
  66. package/src/i18n/en.js +22 -0
  67. package/src/i18n/index.js +31 -1
  68. package/src/i18n/ja.js +25 -1
  69. package/src/i18n/ko.js +25 -2
  70. package/src/i18n/zh.js +25 -1
  71. package/src/lsp/client.js +111 -9
  72. package/src/lsp/servers.js +86 -22
  73. package/src/oneshot.js +325 -65
  74. package/src/pack/sbom.js +28 -5
  75. package/src/pack/selfpack.js +54 -5
  76. package/src/pack/sheet.en.js +24 -3
  77. package/src/pack/tar.js +45 -8
  78. package/src/pack/zip.js +103 -8
  79. package/src/plugins/manage.js +165 -14
  80. package/src/preview/serve.js +239 -23
  81. package/src/providers/bedrock.js +30 -5
  82. package/src/providers/gemini.js +8 -0
  83. package/src/providers/index.js +17 -1
  84. package/src/repl.js +254 -61
  85. package/src/report.js +7 -2
  86. package/src/reset.js +151 -25
  87. package/src/safety/audit.js +108 -8
  88. package/src/safety/authcmd.js +75 -5
  89. package/src/safety/guard.js +507 -15
  90. package/src/safety/hooks.js +172 -20
  91. package/src/safety/keystore.js +178 -18
  92. package/src/safety/network.js +209 -6
  93. package/src/safety/policy.js +377 -36
  94. package/src/safety/runmode.js +9 -2
  95. package/src/safety/secrets.js +243 -17
  96. package/src/safety/shellenv.js +64 -3
  97. package/src/safety/trust.js +298 -32
  98. package/src/safety/undo.js +409 -17
  99. package/src/setup.js +175 -17
  100. package/src/skills/discover.js +122 -19
  101. package/src/stats.js +57 -16
  102. package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
  103. package/src/tools/clipboard.js +53 -20
  104. package/src/tools/convert.js +118 -37
  105. package/src/tools/desc.en.js +48 -5
  106. package/src/tools/doc2md.js +11 -1
  107. package/src/tools/docs.js +94 -18
  108. package/src/tools/edit-match.js +212 -13
  109. package/src/tools/encoding.js +525 -28
  110. package/src/tools/excel-com.js +63 -15
  111. package/src/tools/excel.js +12 -7
  112. package/src/tools/fastgrep.js +444 -36
  113. package/src/tools/fig.js +21 -4
  114. package/src/tools/fsutil.js +235 -21
  115. package/src/tools/hwpxwrite.js +80 -19
  116. package/src/tools/ignore.js +55 -7
  117. package/src/tools/index.js +1029 -145
  118. package/src/tools/jobs.js +325 -50
  119. package/src/tools/kiwi.js +50 -5
  120. package/src/tools/lsp.js +164 -32
  121. package/src/tools/outline.js +52 -12
  122. package/src/tools/pdf.js +366 -56
  123. package/src/tools/shell.js +12 -4
  124. package/src/tools/spawn.js +203 -18
  125. package/src/tools/todo.js +73 -4
  126. package/src/tools/verify.js +312 -43
  127. package/src/tools/webfetch.js +184 -25
  128. package/src/tools/xlsx.js +232 -31
  129. package/src/ui/ansi.js +209 -15
  130. package/src/ui/banner.js +2 -1
  131. package/src/ui/complete.js +46 -4
  132. package/src/ui/diff.js +4 -2
  133. package/src/ui/export.js +91 -17
  134. package/src/ui/inputbox.js +58 -18
  135. package/src/ui/intro.js +9 -2
  136. package/src/ui/level.js +32 -7
  137. package/src/ui/md.js +101 -15
  138. package/src/ui/motion.js +5 -2
  139. package/src/ui/notify.js +7 -1
  140. package/src/ui/office.js +37 -5
  141. package/src/ui/pastechip.js +3 -2
  142. package/src/ui/pick.js +85 -12
  143. package/src/ui/prompt.js +176 -13
  144. package/src/ui/screen.js +2 -1
  145. package/src/ui/spinner.js +6 -1
  146. package/src/ui/status.js +80 -12
  147. package/src/ui/wrap.js +23 -37
  148. /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
  149. /package/src/skills/builtin/{/352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md" → build-deep/SKILL.md} +0 -0
  150. /package/src/skills/builtin/{/354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md" → debug-step-by-step/SKILL.md} +0 -0
  151. /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
  152. /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
  153. /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
  154. /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
  155. /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
package/src/repl.js CHANGED
@@ -7,7 +7,7 @@ import { 받기설정 } from './safety/authcmd.js';
7
7
  import { 주소가리기 } from './safety/secrets.js';
8
8
  import { homedir } from 'node:os';
9
9
  import { resolve, basename } from 'node:path';
10
- import { c, say as 바로쓰기, mark, clip } from './ui/ansi.js';
10
+ import { c, say as 바로쓰기, mark, clip, 화면글거르기 } from './ui/ansi.js';
11
11
  import { headerLines } from './ui/status.js';
12
12
  import { 종, 창제목, 제목되돌리기, 알릴까, 제목글 } from './ui/notify.js';
13
13
  import { 보이기 as 인트로, 기본곁말 } from './ui/intro.js';
@@ -24,10 +24,10 @@ import { Session, repairToolPairs } from './agent/session.js';
24
24
  import { makeScope } from './safety/guard.js';
25
25
  import { 언어서버있나 } from './tools/index.js';
26
26
  import { 모두끄기 as 언어서버다끄기 } from './lsp/client.js';
27
+ import { 임시치우기 } from './tools/convert.js';
27
28
  import { History } from './safety/undo.js';
28
29
  import { Audit, 열쇠묻기 } from './safety/audit.js';
29
- import { activeProfile, load, resolveKey, save as saveCfg, 저장시도, homeDir, 잠금소식, 열쇠탈소식, 프로젝트설정소식 } from './config.js';
30
- import { 프로젝트설정줄들 } from './safety/trust.js';
30
+ import { activeProfile, load, resolveKey, save as saveCfg, 저장시도, homeDir, 열쇠탈소식, 소식줄들 } from './config.js';
31
31
  import { 남길것읽기 } from './safety/shellenv.js';
32
32
  import { discover } from './skills/discover.js';
33
33
  import { allowEndpoint, setOffline, isOffline } from './safety/network.js';
@@ -39,10 +39,10 @@ import { 못박기 } from './agent/pins.js';
39
39
  import { 카드 } from './agent/card.js';
40
40
  import { 배움 } from './agent/evolve.js';
41
41
  import { 마크다운 } from './ui/md.js';
42
- import { askHidden } from './ui/prompt.js';
42
+ import { askHidden, 가림중 } from './ui/prompt.js';
43
43
  import { explain, shows as levelShows } from './ui/level.js';
44
44
  import { 고르기 as 승인고르기, 다음 as 승인다음 } from './ui/approve.js';
45
- import { 추천, 채울글 } from './ui/complete.js';
45
+ import { 추천, 채울글, 붙임탭완성기 } from './ui/complete.js';
46
46
  import { 접어쓰기 } from './ui/wrap.js';
47
47
  import { 접을까 as 붙임접을까, 표만들기 as 붙임표, 펼치기 as 붙임펼치기, 쓴번호들 as 붙임쓴번호들, 남길글, 안쪽최대 as 붙임안쪽최대 } from './ui/pastechip.js';
48
48
  import { 고른것풀기, 계획답풀기 } from './ui/pick.js';
@@ -110,13 +110,15 @@ function toolLabel(name, args) {
110
110
  // 가 도구 줄에 찍힌다. 모델이 그런 이름으로 도구를 부르면 실제로 여기 온다
111
111
  // (loop.js 가 모르는 도구도 showLabel 로 올린다).
112
112
  const g = Object.hasOwn(TOOL_GLYPH, name) ? TOOL_GLYPH[name] : c.cyan('⏺');
113
- const 안 = clip(String(first ?? ''), 56);
113
+ // 경로·명령은 모델이 적은 글이다. 섞인 제어 순서는 화면에 닿기 전에 뗀다(ansi.js 화면글거르기).
114
+ const 안 = clip(화면글거르기(first), 56);
114
115
  return `${g} ${c.bold(name)}${안 ? `${c.gray('(')}${c.gray(안)}${c.gray(')')}` : ''}`;
115
116
  }
116
117
 
117
118
  function toolResultLine(result, ms) {
118
119
  const t = ms > 700 ? c.gray(` ${옮긴말('unit.sec', { n: (ms / 1000).toFixed(1) })}`) : '';
119
- if (result?.error) return `${c.red('└')} ${c.red(clip(String(result.error).split('\n')[0], 80))}${t}`;
120
+ // 오류·요약에는 파일 이름과 도구 출력이 섞여 온다 — 제어 순서를 뗀 뒤 자른다.
121
+ if (result?.error) return `${c.red('└')} ${c.red(clip(화면글거르기(result.error).split('\n')[0], 80))}${t}`;
120
122
 
121
123
  /*
122
124
  * 확인 결과는 색으로 갈라 준다.
@@ -136,7 +138,7 @@ function toolResultLine(result, ms) {
136
138
 
137
139
  // 고친 자리는 몇 줄이 늘고 줄었는지를 요약 옆에 붙인다.
138
140
  const 셈 = result?.diff ? ` ${shortStat(result.diff)}` : '';
139
- return `${c.gray('└')} ${c.gray(clip(result?.summary ?? 옮긴말('sum.done'), 80))}${셈}${t}`;
141
+ return `${c.gray('└')} ${c.gray(clip(화면글거르기(result?.summary ?? 옮긴말('sum.done')), 80))}${셈}${t}`;
140
142
  }
141
143
 
142
144
  // 수준별로 몇 줄까지 펼칠지. 초보에게 60줄을 쏟으면 아무것도 안 읽는다.
@@ -164,32 +166,32 @@ export const 이어쓰기표시 = (l) =>
164
166
 
165
167
  export async function chatLoop(opts = {}) {
166
168
  const cfg = load();
169
+ // 모아 둔 소식을 비운다 (아래 「한 자리에서 비운다」 머리말).
170
+ const 소식쓰기 = () => { for (const 줄 of 소식줄들(cfg)) 바로쓰기(줄); };
167
171
  const prof = activeProfile(cfg);
168
172
  // 연결이 없으면 화면을 세우기 전에 끝난다. 세운 뒤에 나가면 상자를
169
173
  // 그렸다 지우는 제어문자가 이 안내 사이에 끼어 화면이 지저분해진다.
170
174
  if (!prof) {
175
+ // 연결이 없어도 모아 둔 소식은 낸다 — 「이 폴더 설정은 안 믿어서 안 읽었다」 가
176
+ // 바로 연결이 없는 까닭일 수 있다. 아래에서 비우면 여기서 끝나는 판에는 안 나온다.
177
+ 소식쓰기();
171
178
  바로쓰기('');
172
179
  바로쓰기(` ${mark.warn} 저장된 연결이 없습니다. ${c.cyan('deel setup')} 을 먼저 실행하세요.`);
173
180
  바로쓰기('');
174
181
  return 1;
175
182
  }
176
183
 
177
- // 평문으로 있던 열쇠를 방금 잠갔으면 그렇다고 한 줄. 조용히 바꾸면
178
- // 나중에 설정 파일을 열어 본 사람이 제 열쇠가 사라진 줄 안다.
179
- const 잠금 = 잠금소식();
180
- if (잠금) {
181
- 바로쓰기('');
182
- 바로쓰기(` ${mark.ok} ${잠금}`);
183
- }
184
-
185
184
  /*
186
- * 이 폴더의 프로젝트 설정을 안 읽었거나 일부를 걷어냈으면 그렇다고 한 줄.
185
+ * 모아 둔 소식을 한 자리에서 비운다 (config.js 의 소식줄들).
187
186
  *
188
- * 조용히 무시하면 적어 둔 사람은 걸린 줄 알고, 실제로는 안 걸린 채로 일이
189
- * 돈다. 그 어긋남이 설정 전체를 못 믿게 만든다 — 「.deel/config.json 은
190
- * 가끔 먹는 파일」 이 되는 순간 아무도 안 쓴다.
187
+ * 여기는 열쇠를 방금 잠갔다는 말과 프로젝트 설정을 안 읽었다는 말을 **따로**
188
+ * 당기고 있었다. 넷 중 둘만 당겼으니 관리 정책 파일이 깨져도 대화 화면에는
189
+ * 한 줄도 안 나왔다 — 관리자가 건 금지가 통째로 안 걸리는 채로 일이 돈다.
190
+ * 문마다 따로 당기면 이렇게 하나씩 빠진다.
191
+ *
192
+ * 잠근 열쇠를 못 푼 까닭(열쇠탈)은 resolveKey 뒤라야 생기므로 아래에서 한 번 더 본다.
191
193
  */
192
- for (const 줄 of 프로젝트설정줄들(프로젝트설정소식())) 바로쓰기(줄);
194
+ 소식쓰기();
193
195
 
194
196
  /*
195
197
  * 잠근 열쇠를 못 풀었으면 **그 까닭을** 적는다.
@@ -207,21 +209,6 @@ export async function chatLoop(opts = {}) {
207
209
  바로쓰기(` ${mark.warn} ${탈}`);
208
210
  };
209
211
 
210
- /*
211
- * 여기서부터 화면에 나가는 것은 전부 `화면` 을 거친다.
212
- *
213
- * `say` 를 지역 이름으로 다시 묶은 이유: 이 함수 안에 출력이 79군데 있었다.
214
- * 이름을 60번 바꿔 적으면 그중 한둘은 반드시 어긋나고, 어긋난 자리는
215
- * 화면에서만 티가 난다. 이름은 그대로 두고 **가는 곳만** 바꾼다.
216
- * 그러면 갈라내기가 옳은지를 지금 있는 검사들이 그대로 재 준다 —
217
- * 글자 하나라도 달라지면 검사가 잡는다.
218
- *
219
- * 이 파일 밖으로 나가는 이름이 아니다. 화면을 세우기 전에 쓰는 자리는
220
- * 위처럼 `바로쓰기` 를 쓴다.
221
- */
222
- const 화면 = await 화면고르기({ tui: opts.tui });
223
- const say = (s = '') => 화면.줄(s);
224
-
225
212
  const root = opts.root ? opts.root : process.cwd();
226
213
  const conn = {
227
214
  kind: prof.kind, base: prof.baseUrl, auth: prof.auth,
@@ -313,6 +300,29 @@ export async function chatLoop(opts = {}) {
313
300
  */
314
301
  언어잡기({ cfg });
315
302
 
303
+ /*
304
+ * ── 화면은 여기서 세운다. 여기서부터 나가는 것은 전부 `화면` 을 거친다 ──
305
+ *
306
+ * 이 줄은 위(열쇠탈보이기 앞)에 있었다. 그러면 위에 적힌 두 약속이 둘 다
307
+ * 거짓이 된다 — 바로 아래 「화면을 세우기 전에 쓰는 자리는 바로쓰기를 쓴다」
308
+ * 인데 정작 `열쇠탈보이기()` 가 화면을 세운 **뒤**에 바로쓰기로 찍고 있었고,
309
+ * `그림적용` 에 붙은 「화면을 세우기 전에 해야 첫 판부터 맞다」 도 마찬가지였다.
310
+ * 그림·사무실·말은 화면이 그릴 때 읽는 값이라, 정하는 자리가 세우는 자리보다
311
+ * 뒤면 첫 판이 무엇으로 그려질지가 화면 구현에 딸려 간다. 세우기 전에 다
312
+ * 정해 두면 그 물음 자체가 없어진다.
313
+ *
314
+ * `say` 를 지역 이름으로 다시 묶은 이유: 이 함수 안에 출력이 79군데 있었다.
315
+ * 이름을 60번 바꿔 적으면 그중 한둘은 반드시 어긋나고, 어긋난 자리는
316
+ * 화면에서만 티가 난다. 이름은 그대로 두고 **가는 곳만** 바꾼다.
317
+ * 그러면 갈라내기가 옳은지를 지금 있는 검사들이 그대로 재 준다 —
318
+ * 글자 하나라도 달라지면 검사가 잡는다.
319
+ *
320
+ * 이 파일 밖으로 나가는 이름이 아니다. 화면을 세우기 전에 쓰는 자리는
321
+ * 위 열쇠탈보이기() 처럼 `바로쓰기` 를 쓴다.
322
+ */
323
+ const 화면 = await 화면고르기({ tui: opts.tui });
324
+ const say = (s = '') => 화면.줄(s);
325
+
316
326
  const session = new Session(conn, {
317
327
  root,
318
328
  mode: opts.mode ?? 'auto',
@@ -405,7 +415,10 @@ export async function chatLoop(opts = {}) {
405
415
  * 규격 서버가 400 을 내서 이어받자마자 첫 마디에서 죽는다. 손봐서 받는다.
406
416
  */
407
417
  const { messages, 고친것 } = repairToolPairs(적힌것);
418
+ // 진짜로 이어받았나. 아니면 아래에서 이 store 를 놓는다.
419
+ let 이어받았나 = false;
408
420
  if (messages.length) {
421
+ 이어받았나 = true;
409
422
  session.messages = messages;
410
423
  say('');
411
424
  say(` ${mark.ok} ${c.bold(target)} ${c.gray(`— 메시지 ${messages.length}개를 이어 받았습니다.`)}`);
@@ -445,12 +458,63 @@ export async function chatLoop(opts = {}) {
445
458
  const 박힌것 = store.못박은것읽기();
446
459
  if (박힌것.length) {
447
460
  session.못박은것 = new 못박기(박힌것);
448
- say(` ${c.gray(`못 박아 둔 것 ${session.못박은것.개수()}개도 그대로 이어 받았습니다 —`)} ${c.cyan('/pin')}`);
461
+ /*
462
+ * 되살린 것이 다 실리는지 본다 (2.0.0 6회차 · Gemini 못박기6u W4). 되살리기는
463
+ * 더하기 를 안 거쳐 개수·토큰 상한 검사가 안 닿는다 — 1.20.x 에서 박아 둔 긴 핀
464
+ * 14개가 개수 상한에서 12개, 프롬프트에는 3개만 실리는데 화면은 「12개도 그대로
465
+ * 이어 받았습니다」 였다. 사람은 다 지켜진다고 믿고 모델은 9개를 못 받는다.
466
+ */
467
+ const 실림 = session.못박은것.실린것();
468
+ if (실림.개수 < 박힌것.length) {
469
+ say(` ${mark.warn} ${c.yellow(`못 박아 둔 것 ${박힌것.length}개 가운데 ${실림.개수}개만 프롬프트에 실립니다 — 개수·자리 상한을 넘었습니다.`)} ${c.gray('보고 줄이거나 빼려면')} ${c.cyan('/pin')}`);
470
+ } else {
471
+ say(` ${c.gray(`못 박아 둔 것 ${session.못박은것.개수()}개도 그대로 이어 받았습니다 —`)} ${c.cyan('/pin')}`);
472
+ }
449
473
  }
474
+ /*
475
+ * ── 못 이어받았으면 **그 이름을 놓는다** ──────────────────────────
476
+ *
477
+ * 화면은 「새 대화로 시작합니다」 라고 말해 놓고, store 는 방금 못 읽은
478
+ * 그 id 를 그대로 쥐고 있었다. 아래 `if (!store)` 가 참이 아니니 새
479
+ * 대화가 안 열리고, store.begin() 이 **오타난 그 이름으로** 파일을
480
+ * 하나 만든다. 그러면 `/sessions` 목록에 오타가 진짜 대화처럼 끼고,
481
+ * 같은 오타로 한 번 더 이어하면 이번에는 「이어 받았습니다」 가 뜬다 —
482
+ * 사람이 이어받았다고 믿는 대화가 사실은 제 오타로 생긴 빈 파일이다.
483
+ *
484
+ * 못 읽은 까닭이 권한이나 다른 프로그램의 잠금이면 더 나쁘다. 그 자리에
485
+ * 우리가 새로 적기 시작하면 아직 성한 옛 대화 위에 덮어쓸 수도 있다.
486
+ * 못 읽었으면 **안 건드리는 것**이 맞다.
487
+ *
488
+ * 되살린 핀은 그대로 둔다 — 그건 이미 session 에 실렸고, 새로 열리는
489
+ * 대화에 같이 적힌다. 여기서 놓는 것은 **적을 자리**뿐이다.
490
+ */
491
+ if (!이어받았나) store = null;
450
492
  }
451
493
  }
494
+ // 이어받은 자리를 그대로 쓰는가, 새 파일을 여는가. 아래 못 박기 옮겨 적기가 이걸 본다.
495
+ const 이어쓰는자리 = store;
452
496
  if (!store) store = new Store(root);
453
497
  store.begin({ model: conn.model, base: 주소가리기(conn.base), root });
498
+ /*
499
+ * 되살린 못 박기를 **새로 연 대화에 옮겨 적는다** (막판 훑기).
500
+ *
501
+ * 바로 위 머리말이 「되살린 핀은 그대로 둔다 — 새로 열리는 대화에 같이
502
+ * 적힌다」 고 적어 뒀는데, **적는 자리가 없었다.** `begin()` 은 meta 한 줄만
503
+ * 쓰고, `못박기목록` 을 부르는 데는 `/pin` 하나뿐이다(commands/work.js).
504
+ *
505
+ * 그래서 못 이어받은 판 — 파일은 있는데 남은 말이 중단된 도구 호출뿐이라
506
+ * 새 대화로 넘어가는 자리 — 에서 핀이 **이 판에서만** 살아 있었다. 화면은
507
+ * 「그대로 이어 받았습니다」 라고 말해 놓고, 다음에 그 새 대화를 `--resume`
508
+ * 하면 핀이 하나도 없다. 사람은 이미 말했다고 믿으니 다시 말하지 않는다 —
509
+ * 핀을 되살리는 까닭으로 위에 적어 둔 바로 그 고장이다.
510
+ *
511
+ * 이어쓰는 자리면 안 적는다. 그 파일에는 이미 그 목록이 있고, 같은 줄을
512
+ * 한 번 더 얹을 까닭이 없다.
513
+ */
514
+ if (!이어쓰는자리 && session.못박은것?.개수?.()) {
515
+ try { store.못박기목록(session.못박은것.직렬화()); }
516
+ catch { /* 못 적어도 이 판은 돈다 — 다음 이어받기에서만 아쉽다 */ }
517
+ }
454
518
  // 머리말보다 먼저 일어난 탈은 여기 모았다가 아래 경고 자리에서 같이 찍는다.
455
519
  const 켤때경고 = [];
456
520
  // 되돌리기 이력을 못 읽는다는 말은 한 판에 한 번만 한다 — 턴마다 세는 자리라
@@ -463,7 +527,12 @@ export async function chatLoop(opts = {}) {
463
527
  * 계속 실패하면 .deel/sessions 가 끝없이 커지는데 화면에는 영영 안 떴다.
464
528
  * 쓰기 실패는 store 가 세어서 알리는데(아래 저장 경고) 이 자리만 옛 모양이었다.
465
529
  */
466
- try { prune(root); } catch (e) { 켤때경고.push(`오래된 대화 정리를 못 했습니다 — ${clip(String(e?.message ?? e), 60)}`); }
530
+ /*
531
+ * 지금 이어받은 대화는 정리에서 뺀다 (사냥5 H5-2, agent/store.js 의 prune 머리말).
532
+ * 한 달 넘은 대화를 --resume 하면 그 파일은 아직 시각이 옛날이라, 바로 여기서
533
+ * 지워지고 다음 한 줄이 머리글도 옛 대화도 없는 새 파일에 적혔다.
534
+ */
535
+ try { prune(root, { 남길것: [store.id] }); } catch (e) { 켤때경고.push(`오래된 대화 정리를 못 했습니다 — ${clip(String(e?.message ?? e), 60)}`); }
467
536
 
468
537
  /*
469
538
  * 밖에서 붙인 도구(MCP) 서버를 띄운다.
@@ -537,7 +606,8 @@ export async function chatLoop(opts = {}) {
537
606
  * 그래서 readline 에게서는 '줄을 안 건드림' 만 받고, 무엇을 채울지는 우리가
538
607
  * rl.write 로 직접 정한다. rl.write 는 공개 API 라 한글도 안 깨진다.
539
608
  */
540
- completer: 상자쓰나 ? (line) => [[], line] : undefined,
609
+ // 빈 완성기이되 붙여넣은 조각 끝의 탭은 살린다 — readline 이 그 탭을 Tab 키로 보고 삼켰다(ui/complete.js).
610
+ completer: 상자쓰나 ? 붙임탭완성기(process.stdin) : undefined,
541
611
  });
542
612
 
543
613
  // 입력을 큐로 받는다. rl.question 을 겹쳐 쓰면 파이프로 넣을 때 닫혀 버린다.
@@ -673,6 +743,18 @@ export async function chatLoop(opts = {}) {
673
743
  // 상자 안에서 바꾼 줄을 여기서 진짜 줄바꿈으로 되돌린다. 아래 코드는
674
744
  // 전부 평범한 \n 만 본다 — 줄표가 이 아래로는 한 글자도 안 새어 나간다.
675
745
  const l = 펴기(원래줄);
746
+ /*
747
+ * ── 암호 줄은 아무 데도 안 거친다 ──────────────────────────────────
748
+ *
749
+ * 엑셀 암호는 일하는 도중에 묻는다(ui/prompt.js 의 askHidden). 그 줄이 여기 아래를 그대로
750
+ * 지나가면: 파이프 입력에서는 되비추기로 **글자 그대로 찍히고**, 줄 끝이 백틱이면 「이어 쓰기」
751
+ * 로 쌓여 다음에 친 말과 붙어 모델에게 가고, 입력 상자는 「예약됨」 으로 센다. 받는 쪽에 곧장
752
+ * 넘긴다.
753
+ */
754
+ if (가림중(rl)) {
755
+ if (waiter) { const w = waiter; waiter = null; w(l); } else queue.push(l);
756
+ return;
757
+ }
676
758
  if (echo) say(c.gray(l));
677
759
  /*
678
760
  * 붙여넣는 도중의 줄바꿈은 **사람이 Enter 를 친 것이 아니다.**
@@ -1084,8 +1166,11 @@ export async function chatLoop(opts = {}) {
1084
1166
  *
1085
1167
  * 답이 흘러나오는 동안(줄 중간)에는 그려도 답 줄을 덮으므로,
1086
1168
  * 상자 쪽에서 알아서 넘긴다. 글은 그대로 살아 있다.
1169
+ *
1170
+ * 암호를 받는 중이면 그 글을 그리지 않는다 — 친 글자 수만큼 ● 만 보인다. 여기는
1171
+ * readline 의 되비추기(askHidden 이 가로채는 곳)를 안 거치고 rl.line 을 곧장 그린다.
1087
1172
  */
1088
- 화면.대기갱신(펴기(rl.line), queue.length);
1173
+ 화면.대기갱신(가림중(rl) ? '●'.repeat([...펴기(rl.line)].length) : 펴기(rl.line), queue.length);
1089
1174
  }
1090
1175
  });
1091
1176
  });
@@ -1132,7 +1217,9 @@ export async function chatLoop(opts = {}) {
1132
1217
  * 승인으로 읽힌다.
1133
1218
  */
1134
1219
  const ask = async (label, o = {}) => {
1135
- const 앞 = ` ${c.gray('›')} ${label} ${o.def ? c.gray(`[${o.def}] `) : ''}`;
1220
+ // 승인 물음에는 모델이 내민 명령·경로가 실린다. 거기 섞인 `ESC [2K \r` 로 물음을 덮어써 다른 명령을
1221
+ // 승인하게 만들 수 있다 — 물음을 그리기 전에 뗀다(ansi.js 화면글거르기 머리말).
1222
+ const 앞 = ` ${c.gray('›')} ${화면글거르기(label, { 색남김: true })} ${o.def ? c.gray(`[${o.def}] `) : ''}`;
1136
1223
  화면.붙임(앞);
1137
1224
  /*
1138
1225
  * 되묻는 자리는 상자를 안 쓴다 — '실행할까요? (y/n)' 에 테두리를 두르면
@@ -1314,8 +1401,21 @@ export async function chatLoop(opts = {}) {
1314
1401
  // 설정에도, 세션 기록에도, 감사기록에도, 명령줄에도 안 남는다.
1315
1402
  askPassword: async (label) => {
1316
1403
  if (closed) return null;
1317
- const pw = await askHidden(rl, label, nextLine);
1318
- return pw === null || pw === '' ? null : pw;
1404
+ /*
1405
+ * 일하는 도중 미리 쳐 둔 줄은 **암호가 아니다** (4회차 이월).
1406
+ *
1407
+ * nextLine 은 큐에 뭐가 있으면 그것부터 준다. 그래서 「다음은 테스트도 돌려줘」 가 엑셀 암호로
1408
+ * 쓰였다 — 틀린 암호로 한 번을 날리고, 그 지시는 모델에게 영영 안 간다. 위 ask 의 「물음이 뜨기
1409
+ * 전에 쳐 둔 줄은 이 물음의 답이 아니다」 와 같은 까닭·같은 모양이다(파이프는 미리 넣는 것 말고
1410
+ * 답할 길이 없어서 빼지 않는다). 다 묻고 나면 차례 그대로 되돌린다.
1411
+ */
1412
+ const 미리쳐둔 = process.stdin.isTTY ? queue.splice(0) : [];
1413
+ try {
1414
+ const pw = await askHidden(rl, label, nextLine);
1415
+ return pw === null || pw === '' ? null : pw;
1416
+ } finally {
1417
+ if (미리쳐둔.length) queue.unshift(...미리쳐둔);
1418
+ }
1319
1419
  },
1320
1420
  /*
1321
1421
  * ── 이 터미널이 무슨 바이트를 보내는지 본다 (/keys) ──────────────────
@@ -1438,9 +1538,13 @@ export async function chatLoop(opts = {}) {
1438
1538
  // 막혀 있는 것은 기다린 만큼 그대로 손해다. confirm 과 같은 이유로 알린다.
1439
1539
  if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
1440
1540
  창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
1541
+ /*
1542
+ * 입력이 끝났으면 **안 고른 것**이다. `끝나면` 이 없어 def 인 '1' 로 떨어지면,
1543
+ * 아무도 안 고른 판에서 1번을 「사람이 골랐다」 로 모델에게 실어 보낸다.
1544
+ */
1441
1545
  const 답 = String(await ask(
1442
1546
  고를것.length ? 옮긴말('ask.pickPrompt', { 끝: 고를것.length }) : 옮긴말('ask.freePrompt'),
1443
- { def: 고를것.length ? '1' : '' },
1547
+ { def: 고를것.length ? '1' : '', 끝나면: '' },
1444
1548
  )).trim();
1445
1549
  창제목(제목글('도는중', { 폴더: 알림.폴더 }));
1446
1550
 
@@ -1474,8 +1578,20 @@ export async function chatLoop(opts = {}) {
1474
1578
  */
1475
1579
  if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
1476
1580
  창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
1477
- // 멈추라고 누른 것은 **거절**이다. def 인 'y' 로 물러나면 ESC 가 승인이 된다.
1478
- const a = (await ask('실행할까요? (y/n)', { def: 'y', 멈추면: 'n' })).toLowerCase();
1581
+ /*
1582
+ * 멈추라고 누른 것은 **거절**이다. def 인 'y' 로 물러나면 ESC 가 승인이 된다.
1583
+ *
1584
+ * 입력이 **끝난 것**(파이프·Ctrl+D)도 마찬가지로 거절이다. 여태 이 물음만
1585
+ * `끝나면` 이 없어서 `끝나면 ?? def` 가 'y' 로 떨어졌다 — 앞에 사람이 없는
1586
+ * 자리에서 물음이 뜨자마자 저절로 승인되고 명령이 그냥 돌았다. 같은 파일의
1587
+ * 「띄울까요?」·「나가도 될까요?」 는 처음부터 `끝나면: 'n'` 이었다.
1588
+ *
1589
+ * (한동안 여기 「여기만 옛 모양으로 남아 있었다」 고 적혀 있었는데 거짓이었다 —
1590
+ * 계획 승인·이어하기·Ask 도구 셋이 그대로 남아 있었고, 그중 계획 승인은 도구
1591
+ * 하나가 아니라 **계획 전체**를 아무도 답하지 않은 채 승인했다. 지금은 넷 다
1592
+ * 막았다.) 승인은 **답한 사람이 있을 때만** 나는 것이다.
1593
+ */
1594
+ const a = (await ask('실행할까요? (y/n)', { def: 'y', 끝나면: 'n', 멈추면: 'n' })).toLowerCase();
1479
1595
  창제목(제목글('도는중', { 폴더: 알림.폴더 }));
1480
1596
  return a === 'y' || a === 'yes' || a === 'ㅇ';
1481
1597
  },
@@ -1567,7 +1683,19 @@ export async function chatLoop(opts = {}) {
1567
1683
  // 무슨 일이 일어나는 중인지 알려 준다 — 멈춘 것처럼 보이면 안 된다.
1568
1684
  화면.돌리기(옮긴말('run.ctxProbe'));
1569
1685
  let r = null;
1570
- try { r = await probeCtx(conn, { timeout: 6000 }); } catch { /* 못 물어보면 아래에서 처리 */ }
1686
+ /*
1687
+ * 던진 까닭을 **들고 있는다.**
1688
+ *
1689
+ * 여기는 까닭을 안 받고 삼키면서 「못 물어보면 아래에서 처리」 라고 적어
1690
+ * 두었는데, 아래는 그걸 처리할 수가 없었다 — 던지면 r 이 null 이라 `r?.why` 가
1691
+ * 통째로 안 걸린다. 프로필에 ctx 가 적혀 있는 판에서는 그래서 **경고가 한
1692
+ * 줄도 안 나갔고**, 저장된 숫자가 방금 서버에서 확인한 값처럼 머리말에 떴다.
1693
+ * probeCtx 는 두드리다 난 탈은 스스로 삼키지만, 두드릴 자리를 짓는 자리
1694
+ * (모델 이름을 주소에 싣는 encodeURIComponent)는 그 밖이라 여기로 던진다.
1695
+ */
1696
+ let 던진탈 = null;
1697
+ try { r = await probeCtx(conn, { timeout: 6000 }); } catch (e) { 던진탈 = String(e?.message ?? e); }
1698
+ const 못한까닭 = r?.why ?? 던진탈;
1571
1699
  화면.돌림멈춤('');
1572
1700
  if (r?.value) {
1573
1701
  const 전 = conn.ctx;
@@ -1627,8 +1755,8 @@ export async function chatLoop(opts = {}) {
1627
1755
  * 없다」 는 사람이 할 일이 셋 다 다르다. 여태 이 자리에서 그걸 버리고
1628
1756
  * 늘 「서버가 안 알려줍니다」 한 문장으로 뭉갰다.
1629
1757
  */
1630
- 길이경고.push(`컨텍스트를 못 알아냈습니다 ${c.gray('(' + (r?.why ?? '까닭 모름') + ')')} — 우선 ${CTX_DEFAULT.toLocaleString()} 으로 잡았습니다. ${c.cyan('/ctx 655360')} 처럼 직접 지정하세요`);
1631
- } else if (r?.why && !r?.tried?.some((t) => t.ok)) {
1758
+ 길이경고.push(`컨텍스트를 못 알아냈습니다 ${c.gray('(' + (못한까닭 ?? '까닭 모름') + ')')} — 우선 ${CTX_DEFAULT.toLocaleString()} 으로 잡았습니다. ${c.cyan('/ctx 655360')} 처럼 직접 지정하세요`);
1759
+ } else if (못한까닭 && !r?.tried?.some((t) => t.ok)) {
1632
1760
  /*
1633
1761
  * 프로필에 값이 적혀 있으면 **아무 말도 안 하던** 자리다.
1634
1762
  *
@@ -1642,7 +1770,7 @@ export async function chatLoop(opts = {}) {
1642
1770
  * 켤 때마다 경고를 붙이면 곧 아무도 안 읽는다. 여기서 말하는 것은
1643
1771
  * **아무 데서도 대답을 못 받은** 판뿐이다.
1644
1772
  */
1645
- 길이경고.push(`컨텍스트를 서버에 못 물어봤습니다 ${c.gray('(' + r.why + ')')}`
1773
+ 길이경고.push(`컨텍스트를 서버에 못 물어봤습니다 ${c.gray('(' + 못한까닭 + ')')}`
1646
1774
  + ` — 프로필에 적힌 ${c.white(Number(prof.ctx).toLocaleString())} 을 그대로 씁니다. 서버에서 바뀌었으면 ${c.cyan('/ctx auto')}`);
1647
1775
  }
1648
1776
  };
@@ -1692,7 +1820,9 @@ export async function chatLoop(opts = {}) {
1692
1820
  // 안 알려준 것을 아는지 모른다 — 그게 불안하다.
1693
1821
  {
1694
1822
  const 기억 = 기억읽기(root);
1695
- if (기억.줄들.length) 길이알림.push(`지난 대화에서 정한 것 ${기억.줄들.length}개를 들고 시작합니다 — ${c.cyan('/memory')}`);
1823
+ // 안 믿는 폴더에 딸려 온 기억은 안 싣는다(agent/memory.js). 그때 「들고 시작합니다」 는 거짓말이다.
1824
+ if (기억.안믿음) warn.push(`이 폴더의 기억(.deel/memory.md)은 안 실었습니다 — 믿는 폴더가 아니고 이 PC 의 deel 이 적은 그대로도 아닙니다. 실으려면 ${c.cyan('deel trust')}`);
1825
+ else if (기억.줄들.length) 길이알림.push(`지난 대화에서 정한 것 ${기억.줄들.length}개를 들고 시작합니다 — ${c.cyan('/memory')}`);
1696
1826
  }
1697
1827
  if (mcp붙임.서버들.length) {
1698
1828
  const 도구수 = mcp붙임.서버들.reduce((n, s) => n + s.도구.length, 0);
@@ -1708,6 +1838,13 @@ export async function chatLoop(opts = {}) {
1708
1838
  if (found.안믿음) {
1709
1839
  warn.push(`이 폴더의 스킬은 안 읽었습니다 — 믿는 폴더가 아닙니다. 읽게 하려면 ${c.cyan('deel trust')}`);
1710
1840
  }
1841
+ // 에이전트 정의와 내 명령을 덮으려던 저장소 명령도 같은 문을 지났다 (agent/agents.js · skills/discover.js).
1842
+ if (에이전트정보.안믿음) {
1843
+ warn.push(`이 폴더의 에이전트 정의는 안 읽었습니다 — 믿는 폴더가 아닙니다. 읽게 하려면 ${c.cyan('deel trust')}`);
1844
+ }
1845
+ if (found.안믿은명령?.length) {
1846
+ warn.push(`이 폴더의 명령 ${found.안믿은명령.map((n) => c.white(`/${n}`)).join(' · ')} 은 내 명령과 이름이 겹쳐 안 읽었습니다 — 믿는 폴더가 아닙니다`);
1847
+ }
1711
1848
  if (!conn.tools) warn.push(옮긴말('run.noTools'));
1712
1849
  if (!conn.streaming) warn.push(옮긴말('run.noStream'));
1713
1850
  warn.push(...켤때경고);
@@ -2100,7 +2237,20 @@ export async function chatLoop(opts = {}) {
2100
2237
  let 접는중 = false;
2101
2238
  const 접기멈춤 = () => { if (접는중) { 화면.돌림멈춤(); 접는중 = false; } };
2102
2239
 
2103
- const clearThinking = () => { 화면.임시지움(); thinkingShown = false; };
2240
+ /*
2241
+ * 생각이 끝나면 **몇 자 생각했는지 한 줄 남긴다.**
2242
+ *
2243
+ * 도는 동안의 줄(화면.생각)은 임시라 곧 지워지고, 파이프·기록에서는 아예 안 그려진다.
2244
+ * 그래서 모델이 생각을 했는지조차 화면에 남지 않았다 — 느린 턴의 까닭이 안 보인다.
2245
+ */
2246
+ let 생각남긴수 = 0;
2247
+ const clearThinking = () => {
2248
+ 화면.임시지움(); thinkingShown = false;
2249
+ if (thinkChars > 생각남긴수) {
2250
+ say(` ${mark.think} ${c.gray(옮긴말('run.thoughtChars', { n: thinkChars.toLocaleString() }))}`);
2251
+ 생각남긴수 = thinkChars;
2252
+ }
2253
+ };
2104
2254
  // 단계 꼬리표 — 붙을 때만 뒤에 한 칸을 같이 붙인다. 쉬움 수준에서는 빈 글자라
2105
2255
  // '생각 중…' 앞에 빈칸 두 개가 뜨는 일이 없다.
2106
2256
  const 꼬리표 = (ev) => { const t = stageTag(ev, session.level); return t ? t + ' ' : ''; };
@@ -2156,6 +2306,7 @@ export async function chatLoop(opts = {}) {
2156
2306
  case 'stage':
2157
2307
  stage = ev;
2158
2308
  thinkChars = 0;
2309
+ 생각남긴수 = 0;
2159
2310
  break;
2160
2311
 
2161
2312
  // 다시 부르는 이유. 쉬움 수준에서는 토큰 숫자를 안 꺼낸다 —
@@ -2430,12 +2581,23 @@ export async function chatLoop(opts = {}) {
2430
2581
  case 'task_start':
2431
2582
  clearThinking();
2432
2583
  if (streamed) { 답비우기(); say(''); streamed = false; }
2433
- 화면.일바꿈('하위', clip(ev.목적, 24));
2584
+ /*
2585
+ * 목적은 **모델이 적은 글**이다 — 그리기 전에 제어 순서를 뗀다.
2586
+ *
2587
+ * 이 파일은 모델이 적은 경로·명령·요약을 화면에 올릴 때마다 원천에서
2588
+ * 걸러 왔다(112·120·140줄). 여기 셋만 날것으로 나갔다. 지금은 받는
2589
+ * 쪽이 둘 다 막고 있어 새지는 않지만(inputbox.js 의 걸러 · ansi.js 의
2590
+ * say), 그 둘은 **우리 색은 남긴다** — 모델이 적어 넣은 ESC[3xm 은
2591
+ * 그대로 통과해 뒤따르는 줄까지 물들인다. 한 줄 요약이 화면 색을
2592
+ * 바꿀 까닭이 없다. 막는 자리가 하나 더 있어서 나쁠 것도 없다.
2593
+ */
2594
+ const 하위목적 = 화면글거르기(String(ev.목적 ?? ''));
2595
+ 화면.일바꿈('하위', clip(하위목적, 24));
2434
2596
  // 사무실은 도는 하위 작업 수만큼 자리를 채운다.
2435
2597
  도는하위 += 1;
2436
2598
  화면.하위갱신(도는하위);
2437
2599
  say('');
2438
- say(` ${c.hmagenta('⌥')} ${c.bold('하위 작업')} ${c.white(clip(ev.목적, 60))}`
2600
+ say(` ${c.hmagenta('⌥')} ${c.bold('하위 작업')} ${c.white(clip(하위목적, 60))}`
2439
2601
  + ` ${c.gray(`· ${보일이름(ev.모드)} · 최대 ${ev.steps}걸음`)}`);
2440
2602
  say(` ${c.gray('여기서부터는 따로 떨어진 대화입니다 — 결과 요약만 위로 올라옵니다.')}`);
2441
2603
  /*
@@ -2463,7 +2625,7 @@ export async function chatLoop(opts = {}) {
2463
2625
  stuck: '헛돌아서 스스로 멈췄습니다 — 다 못 했습니다',
2464
2626
  aborted: '중단했습니다' }[끝.type] ?? '끝난 이유를 알 수 없습니다';
2465
2627
  say('');
2466
- say(` ${잘됨 ? c.green('✓') : c.yellow('⚠')} ${c.gray('하위 작업')} ${c.white(clip(ev.목적, 50))}`
2628
+ say(` ${잘됨 ? c.green('✓') : c.yellow('⚠')} ${c.gray('하위 작업')} ${c.white(clip(화면글거르기(String(ev.목적 ?? '')), 50))}`
2467
2629
  + ` ${c.gray(`— ${왜}`)} ${c.gray(`(${끝.steps ?? 0}걸음${ev.모델 ? ` · ${ev.모델}` : ''})`)}`);
2468
2630
  // 무엇이 실제로 생겼는지는 하위가 한 말이 아니라 디스크가 말한다.
2469
2631
  만든파일보이기(끝.files);
@@ -2489,7 +2651,7 @@ export async function chatLoop(opts = {}) {
2489
2651
  break;
2490
2652
 
2491
2653
  /*
2492
- * 종합 모드가 **단계를 옮겼다** (agent/단계.js).
2654
+ * 종합 모드가 **단계를 옮겼다** (agent/phase.js).
2493
2655
  *
2494
2656
  * 이 줄이 없으면 사람은 화면의 모드 표시가 턴 도중에 왜 바뀌었는지
2495
2657
  * 모른다 — 자기가 누른 적도 없는데 `~디버그` 가 `~코드` 로 바뀌어
@@ -2595,12 +2757,21 @@ export async function chatLoop(opts = {}) {
2595
2757
  case 'compacted': {
2596
2758
  접기멈춤();
2597
2759
  const 줄인 = ev.before - ev.after;
2598
- say(` ${c.cyan('◱')} ${c.gray(`대화 ${ev.folded}개를 요약으로 접었습니다 — `)}` +
2760
+ /*
2761
+ * 요약을 못 받고 옛 대화를 **잘라 낸** 것을 「요약으로 접었습니다」 라고 먼저 적고
2762
+ * 그 아래 노란 줄로 뒤집고 있었다. 첫 줄만 읽는 사람은 요약이 남은 줄 안다 —
2763
+ * 실제로는 앞선 대화가 없어졌다. 한 일을 첫 줄에 적고, 못 받은 까닭을 붙인다.
2764
+ */
2765
+ const 한일 = ev.fallback
2766
+ ? c.yellow(`요약을 못 받아 옛 대화 ${ev.folded}개를 잘라 냈습니다 — `)
2767
+ : c.gray(`대화 ${ev.folded}개를 요약으로 접었습니다 — `);
2768
+ say(` ${c.cyan('◱')} ${한일}` +
2599
2769
  `${c.gray(ev.before.toLocaleString())} ${c.gray('→')} ${c.white(ev.after.toLocaleString())} ${c.gray('토큰')} ` +
2600
2770
  `${c.green(`(${Math.round((줄인 / Math.max(1, ev.before)) * 100)}% 줄어듦)`)}`);
2601
- if (ev.fallback) say(` ${c.yellow('요약을 못 받아 그냥 줄였습니다.')}`);
2771
+ if (ev.fallback && ev.why) say(` ${c.gray(clip(String(ev.why), 100))}`);
2602
2772
  // 접히면 이력이 통째로 바뀐다. 덧붙이기로는 못 맞추니 새로 적는다.
2603
- ctx.갈래.현재store().replace(session.messages, `압축 — ${ev.folded}개를 요약으로`);
2773
+ ctx.갈래.현재store().replace(session.messages,
2774
+ ev.fallback ? `압축 못 함 — 옛 대화 ${ev.folded}개를 잘라 냄` : `압축 — ${ev.folded}개를 요약으로`);
2604
2775
  saved = session.messages.length;
2605
2776
  break;
2606
2777
  }
@@ -3051,9 +3222,18 @@ export async function chatLoop(opts = {}) {
3051
3222
  // 계획을 다 내놓고 사람 답을 기다리는 자리다. 위 confirm 과 같은 이유로 알린다.
3052
3223
  if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
3053
3224
  창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
3225
+ /*
3226
+ * 입력이 **끝난 것**(파이프·Ctrl+D)은 승인이 아니다 — 위 「실행할까요?」 와 같다.
3227
+ *
3228
+ * 그쪽 머리말이 「같은 파일의 다른 물음들은 처음부터 끝나면:'n' 이었고 여기만
3229
+ * 옛 모양으로 남아 있었다」 고 적으며 고쳤는데, 그 말이 사실이 아니었다.
3230
+ * 계획 승인은 그대로 남아 `끝나면 ?? def` 가 'y' 로 떨어졌다 — 도구 하나
3231
+ * 승인받는 자리는 막아 놓고 계획 **전체**를 승인하는 자리가 열려 있었다.
3232
+ * 재 보니 아무도 답하지 않은 판에서 계획이 승인되고 파일이 만들어졌다.
3233
+ */
3054
3234
  const 답 = String(await ask(
3055
3235
  `${옮긴말('plan.ask')} ${c.gray(옮긴말('plan.hint'))}`,
3056
- { def: 'y' },
3236
+ { def: 'y', 끝나면: 'n', 멈추면: 'n' },
3057
3237
  )).trim();
3058
3238
  /*
3059
3239
  * ── 골라진 것 + 하고 싶은 말 ────────────────────────────────────
@@ -3153,9 +3333,10 @@ export async function chatLoop(opts = {}) {
3153
3333
  say('');
3154
3334
  if (알릴까({ 물어봄: true, 켬: 알림.켬 })) 종();
3155
3335
  창제목(제목글('물어봄', { 폴더: 알림.폴더 }));
3336
+ // 여기도 같다 — 아무도 답하지 않은 판에서 모델을 한 판 더 부르면 안 된다.
3156
3337
  const 답 = String(await ask(
3157
3338
  `이어서 할까요? ${c.gray('⏎ 이어서 · n 그만')}`,
3158
- { def: 'y' },
3339
+ { def: 'y', 끝나면: 'n', 멈추면: 'n' },
3159
3340
  )).trim().toLowerCase();
3160
3341
 
3161
3342
  if (['n', 'no', 'ㄴ', '취소', '그만', '아니', '아니요', '아니오'].includes(답)) {
@@ -3201,6 +3382,18 @@ export async function chatLoop(opts = {}) {
3201
3382
  const 껐다 = await 미리보기끄기();
3202
3383
  if (껐다) say(` ${mark.ok} ${c.gray(`미리보기를 껐습니다 (${껐다.서버.url}).`)}`);
3203
3384
  }
3385
+ /*
3386
+ * 문서를 글로 바꾸며 떨군 임시 파일도 거둔다.
3387
+ *
3388
+ * convert.js 는 바꾼 글을 읽자마자 그 자리에서 지운다. 그런데 윈도우에서
3389
+ * soffice 가 파일을 아직 물고 있으면 그 rmSync 가 실패하고, 거기 붙은 주석은
3390
+ * 「못 지우면 임시치우기가 거둔다」 였다. **거두는 자를 부르는 데가 한 군데도
3391
+ * 없었다.** 그래서 사람 문서의 알맹이가 든 .txt 가 `.deel/tmp` 에 그대로 쌓였다.
3392
+ *
3393
+ * 아무 말도 안 한다. 우리가 떨군 것을 우리가 치우는 일이라 사람이 알 일이 없고,
3394
+ * 못 거둬도 다음 세션이 다시 지나간다.
3395
+ */
3396
+ try { 임시치우기(root); } catch { /* 못 거둬도 대화를 끝내는 데는 지장 없다 */ }
3204
3397
  // 끝맺음은 화면을 접기 **전에** 그린다. close() 가 상자를 걷어내므로,
3205
3398
  // 그 뒤에 찍으면 걷어낸 자리에 뜬금없이 한 줄이 남는다.
3206
3399
  say('');
package/src/report.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // 진단 결과를 화면에 표로 그리고, "이걸로 돌릴 수 있는지" 판정한다.
2
2
  import { c, say, rule, pad, width, mark } from './ui/ansi.js';
3
3
  import { 주소가리기 } from './safety/secrets.js';
4
- import { 프록시고르기, 프록시설정 } from './backend/proxy.js';
4
+ import { 프록시고르기, 프록시설정, 프록시비켜가나 } from './backend/proxy.js';
5
5
  import { 규격이름 } from './backend/adapter.js';
6
6
  import { 보관방식 } from './safety/keystore.js';
7
7
  import { load, activeProfile } from './config.js';
@@ -13,11 +13,12 @@ function 열쇠줄() {
13
13
 
14
14
  // 이 주소로 갈 때 거칠 프록시를 한 줄로. 안 거치면 null — 그때는 줄 자체를 안 만든다.
15
15
  // 적어 놨는데 못 쓰는 것(socks5 등)이면 그 까닭을 적는다 — 진단 보고서에서 제일 먼저 볼 줄이다.
16
+ // 다만 루프백·NO_PROXY 처럼 쓸 만했어도 안 거칠 주소면 적지 않는다 — 상관없는 프록시를 탓하게 된다.
16
17
  function 프록시줄(base) {
17
18
  const p = 프록시고르기(base);
18
19
  if (p) return `${p.url} (${p.출처})`;
19
20
  const 탈 = 프록시설정().탈;
20
- return 탈 ? `못 씀 — ${탈}` : null;
21
+ return 탈 && !프록시비켜가나(base) ? `못 씀 — ${탈}` : null;
21
22
  }
22
23
 
23
24
  const ICON = { ok: mark.ok, no: mark.no, warn: mark.warn, skip: c.gray('·') };
@@ -79,7 +80,11 @@ export function verdict(facts, results) {
79
80
 
80
81
  if (get('json') !== 'ok') notes.push('구조적 출력이 약합니다 — 편집 형식을 프롬프트로 강제하고 검사를 붙입니다.');
81
82
  if (get('stream') !== 'ok') notes.push('스트리밍이 없습니다 — 화면은 스피너로 대체하고 기능은 동일하게 갑니다.');
83
+ // 옆줄(json·stream)과 달리 여기만 `=== 'warn'` 이라, **제일 나쁜 판**(system 을 붙이면
84
+ // 요청 자체가 실패하는 게이트웨이)에서 판정 글이 한 마디도 안 했다. 표는 길고 판정 글은
85
+ // 짧아서 사람은 판정 글만 읽는다 — 규칙도 스킬도 안 걸리는 연결이 「준비됨」 으로 나갔다.
82
86
  if (get('system') === 'warn') notes.push('시스템 지시를 약하게 따릅니다 — 스킬을 적게, 짧게 올려야 합니다.');
87
+ else if (get('system') !== 'ok') notes.push('시스템 메시지를 붙이면 요청이 실패합니다 — 규칙·스킬이 하나도 안 걸립니다. 게이트웨이가 system 역할을 받는지 확인이 필요합니다.');
83
88
  if (get('think') === 'ok') notes.push('추론 강도가 모델 층에서 적용됩니다 — /think 로 바로 조절됩니다.');
84
89
  else notes.push('추론 강도는 루프 층(계획 강제·도구 호출 상한·자기검증 횟수)으로 조절합니다.');
85
90
  if (!facts.ctx) notes.push('컨텍스트 길이를 서버가 안 알려줍니다 — 설정에서 직접 넣어야 합니다.');