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/acp/serve.js CHANGED
@@ -27,8 +27,8 @@
27
27
  // 그래서 여기서 process.stdout.write 를 통째로 바꿔 끼운다. 부르는 자리를
28
28
  // 하나하나 찾아 막는 방법도 있지만, 그건 앞으로 새로 쓰는 코드까지 계속
29
29
  // 조심해야 한다는 뜻이다 — 언젠가 반드시 한 군데를 빠뜨린다.
30
- import { existsSync } from 'node:fs';
31
- import { join, resolve } from 'node:path';
30
+ import { existsSync, statSync } from 'node:fs';
31
+ import { isAbsolute, join, resolve } from 'node:path';
32
32
  import { VERSION } from '../version.js';
33
33
  import { 규칙모으기, 늘허락, 정책읽기 } from '../safety/policy.js';
34
34
  import { 훅읽기 } from '../safety/hooks.js';
@@ -36,12 +36,12 @@ import { 에이전트읽기 } from '../agent/agents.js';
36
36
  import { 남길것읽기 } from '../safety/shellenv.js';
37
37
  import { 받기설정 } from '../safety/authcmd.js';
38
38
  import { run } from '../agent/loop.js';
39
- import { Session, repairToolPairs } from '../agent/session.js';
40
- import { Store, sessionsDir, prune } from '../agent/store.js';
39
+ import { Session, repairToolPairs, 규격맞추기 } from '../agent/session.js';
40
+ import { Store, sessionsDir, prune, 대화이름인가 } from '../agent/store.js';
41
41
  import { makeScope } from '../safety/guard.js';
42
42
  import { History } from '../safety/undo.js';
43
43
  import { Audit, 열쇠묻기 } from '../safety/audit.js';
44
- import { activeProfile, load, resolveKey, homeDir, save as saveCfg } from '../config.js';
44
+ import { activeProfile, load, resolveKey, homeDir, save as saveCfg, 소식줄들 } from '../config.js';
45
45
  import { 말 as 옮긴말 } from '../i18n/index.js';
46
46
  import { 알림채움, 알림말 } from '../backend/retry.js';
47
47
  import { 전선붙이기, 세션이름짓기 } from '../backend/wire.js';
@@ -57,7 +57,7 @@ import { 배움 } from '../agent/evolve.js';
57
57
  import { 카드 } from '../agent/card.js';
58
58
  import { 못박기 } from '../agent/pins.js';
59
59
  import { route } from '../agent/route.js';
60
- import { ORDER as 모드순서, get as getWork, normalize as 모드정리 } from '../agent/modes.js';
60
+ import { ORDER as 모드순서, get as getWork, normalize as 모드정리, 보일이름 } from '../agent/modes.js';
61
61
  import { 모두끝내기 as 일감모두끝내기 } from '../tools/jobs.js';
62
62
  import { 연결, 줄나누기, 모르는방법오류, 잘못된인자오류, 인증필요오류 } from './jsonrpc.js';
63
63
  import { 도구시작, 도구끝남, 도구이름표, 도구갈래, 도구자리, 멈춘까닭, 프롬프트글, 되살린것 } from './map.js';
@@ -124,6 +124,43 @@ export async function acp(opts = {}) {
124
124
  /** 세션 하나. 에디터의 탭 하나에 해당한다. */
125
125
  const 방들 = new Map();
126
126
 
127
+ /*
128
+ * ── 이 프로세스가 연 연결 주소 전부 (6회차 N9) ──────────────────────────
129
+ *
130
+ * 문지기(safety/network.js)의 allowEndpoint 는 「이전에 올린 것은 지운다」 — 부르는
131
+ * 쪽이 **지금 열려 있어야 할 것 전부**를 한 번에 말하라는 약속이다. 여기는 방마다
132
+ * 제 주소 하나만 말했다. 에디터에서 연결이 다른 프로젝트 둘을 열면 둘째 탭을 여는
133
+ * 순간 첫 탭의 주소가 지워져, 첫 탭의 다음 한마디가 「허용되지 않은 주소입니다」 로
134
+ * 막혔다.
135
+ *
136
+ * 방들 에서 모으지 않는 까닭: 방은 **다 만들어진 뒤에야** 방들 에 들어간다(아래 열린자리
137
+ * 머리말). 겹쳐 온 session/new 둘이 서로 상대 주소를 못 보고 지운다. 그래서 허락을
138
+ * 통과한 그 자리에서 바로 적는다. 넓어지는 것이 아니다 — 여기 오르는 주소는 저마다
139
+ * 이 프로세스의 봉인·허락 검사를 지난 것뿐이다.
140
+ */
141
+ const 열린주소 = new Set();
142
+
143
+ /*
144
+ * ── 이 프로세스가 내준 대화 이름 → 그 폴더 (사냥5 H5-1) ──────────────────
145
+ *
146
+ * 대화 이름은 초 단위이고, 빈 이름인지는 **그 폴더의 대화 파일**로만 봤다. 그런데
147
+ * 에디터 하나가 이 프로세스로 여러 프로젝트를 연다. 두 프로젝트를 같은 초에 열면
148
+ * 두 폴더 다 그 이름의 파일이 없어서 두 방이 같은 sessionId 를 받았고, 방들 에는
149
+ * 뒤엣것만 남았다. 그 뒤로 A 탭에 한 말이 B 폴더의 대화 파일에 적히고, 모델은 B 를
150
+ * 작업 폴더로 알았다 — 파일을 고치면 **남의 프로젝트**를 고친다.
151
+ *
152
+ * sessionId 는 에디터가 방을 찾는 유일한 열쇠이고, 껐다 켤 때 파일을 찾는 이름이기도
153
+ * 해서 폴더를 섞어 지을 수 없다. 그래서 이 프로세스 안에서는 **안 겹치게** 짓고,
154
+ * 이미 다른 폴더로 연 이름을 또 다른 폴더로 되살리라면 거절한다.
155
+ * 방들 에는 방이 **다 만들어진 뒤에야** 들어가므로(그 사이 MCP 를 붙이느라 기다린다)
156
+ * 겹쳐 온 session/new 둘을 가르려면 이름을 짓는 그 자리에서 바로 적어 둬야 한다.
157
+ */
158
+ const 열린자리 = new Map();
159
+ const 같은자리 = (가, 나) => {
160
+ const 편 = (p) => { const r = resolve(String(p)); return process.platform === 'win32' ? r.toLowerCase() : r; };
161
+ return 편(가) === 편(나);
162
+ };
163
+
127
164
  /*
128
165
  * 폴더 → MCP 붙임. **폴더 하나에 한 벌**이다.
129
166
  *
@@ -158,9 +195,44 @@ export async function acp(opts = {}) {
158
195
  * @param {string|null} o.아이디 이어할 대화 이름. 주면 그 파일에 이어 쓴다
159
196
  */
160
197
  async function 방만들기(요청, { 아이디 = null } = {}) {
161
- const cfg = load();
198
+ /*
199
+ * 작업 폴더를 **설정보다 먼저** 정한다.
200
+ *
201
+ * 여기는 `load()` 가 위에 있었다. 그러면 설정·신뢰·승인 규칙은 이
202
+ * 프로세스를 띄운 자리(process.cwd())를 기준으로 읽히고, 파일을 고치는
203
+ * 쪽만 아래에서 정한 `root` 를 쓴다 — 한 세션 안에서 두 폴더가 섞인다.
204
+ * 자세한 것은 config.js 의 projectDir 머리말에 적어 뒀다.
205
+ */
206
+ /*
207
+ * 에디터가 준 cwd 는 **있는 절대 경로**여야 한다 — 아래 「작업 폴더는 에디터가 정한다」.
208
+ *
209
+ * 여기가 받은 글을 그대로 썼다. 상대 경로(`rel/dir`)면 에디터가 연 프로젝트가 아니라
210
+ * **이 프로세스를 띄운 자리** 기준으로 풀려서, 그 아래 머리말이 「제일 무서운 종류의
211
+ * 실수」 라고 적은 그대로 엉뚱한 폴더를 고친다. 없는 절대 경로면 대화 폴더를 만들다
212
+ * 그 경로를 통째로 새로 만들었다. 둘 다 에디터가 잘못 준 것이니 잘못된 인자로 답한다.
213
+ * 안 주면 예전처럼 --root(없으면 띄운 자리)를 쓴다 — 그건 사람이 띄울 때 정한 것이다.
214
+ */
215
+ const 준자리 = typeof 요청?.cwd === 'string' && 요청.cwd ? 요청.cwd : null;
216
+ if (준자리 != null) {
217
+ if (!isAbsolute(준자리)) throw 잘못된인자오류(`cwd 는 절대 경로여야 합니다: ${준자리}`);
218
+ let 폴더인가 = false;
219
+ try { 폴더인가 = statSync(준자리).isDirectory(); } catch { /* 없다 */ }
220
+ if (!폴더인가) throw 잘못된인자오류(`cwd 가 있는 폴더가 아닙니다: ${준자리}`);
221
+ }
222
+ const root = 준자리 ?? (opts.root ?? process.cwd());
223
+ const cfg = load({ root });
224
+ // 모아 둔 소식을 로그로 비운다 (아래 conn 을 지은 자리의 머리말). 색 제어문자는 뺀다.
225
+ const 소식로그 = () => {
226
+ for (const 줄 of 소식줄들(cfg)) {
227
+ const 맨글 = String(줄).replace(/\x1b\[[0-9;]*m/g, '').trim();
228
+ if (맨글) 로그(맨글);
229
+ }
230
+ };
162
231
  const prof = activeProfile(cfg);
163
232
  if (!prof) {
233
+ // 연결이 없어 여기서 끝나도 소식은 로그에 남긴다 — 「이 폴더 설정은 안 믿어서
234
+ // 안 읽었다」 가 바로 연결이 없는 까닭일 수 있다.
235
+ 소식로그();
164
236
  /*
165
237
  * 잘못된인자가 아니라 **인증필요**다.
166
238
  *
@@ -181,7 +253,6 @@ export async function acp(opts = {}) {
181
253
  * 띄운 자리를 기준으로 파일을 찾게 된다. 그러면 도구는 멀쩡히 도는데
182
254
  * 엉뚱한 폴더를 고친다. 그게 제일 무서운 종류의 실수다.
183
255
  */
184
- const root = typeof 요청?.cwd === 'string' && 요청.cwd ? 요청.cwd : (opts.root ?? process.cwd());
185
256
 
186
257
  /*
187
258
  * 대화는 폴더 안(.deel/sessions/)에 남는다 — 터미널에서 하던 것과 같은 자리다.
@@ -194,7 +265,12 @@ export async function acp(opts = {}) {
194
265
  if (아이디 && !existsSync(join(sessionsDir(root), `${아이디}.jsonl`))) {
195
266
  throw 잘못된인자오류(`그런 대화가 없습니다: ${아이디} (${root})`);
196
267
  }
197
- const store = new Store(root, 아이디);
268
+ // 이 이름이 이 프로세스에서 이미 다른 폴더로 열려 있으면 되살리지 않는다 (위 열린자리 머리말).
269
+ const 먼저연곳 = 아이디 ? 열린자리.get(아이디) : undefined;
270
+ if (먼저연곳 !== undefined && !같은자리(먼저연곳, root)) {
271
+ throw 잘못된인자오류(`이 대화 이름은 이 창에서 이미 다른 폴더로 열려 있습니다: ${아이디} (${먼저연곳}) — 폴더가 다른 두 대화를 한 이름으로 섞지 않습니다.`);
272
+ }
273
+ const store = new Store(root, 아이디, { 피할것: (id) => 열린자리.has(id) });
198
274
 
199
275
  const conn = {
200
276
  kind: prof.kind, base: prof.baseUrl, auth: prof.auth,
@@ -236,6 +312,16 @@ export async function acp(opts = {}) {
236
312
  인증서: 인증서설정(prof),
237
313
  열쇠받기: 받기설정(prof, { 정책값: 정책읽기().값 }),
238
314
  };
315
+ /*
316
+ * 모아 둔 소식을 로그로 비운다 (config.js 의 소식줄들).
317
+ *
318
+ * 이 문은 넷 중 하나도 안 당겼다. 에디터 화면에는 안 뜨지만 에디터의 로그
319
+ * 창에는 남는다 — 안 비우면 관리 정책이 깨지거나 이 폴더 설정을 걷어내도
320
+ * 어디에도 흔적이 없다. 열쇠탈은 resolveKey 뒤라야 생기므로 conn 을 지은 여기서.
321
+ * 로그에는 색 제어문자를 뺀다.
322
+ */
323
+ 소식로그();
324
+
239
325
  /*
240
326
  * 바깥으로 나가는 연결은 허가가 있어야 연다 (safety/runmode.js).
241
327
  *
@@ -257,7 +343,9 @@ export async function acp(opts = {}) {
257
343
  `이 연결은 이 컴퓨터 밖으로 나갑니다 (${주소가리기(어디)}).`
258
344
  + ' 터미널에서 deel 을 한 번 켜서 허락하거나, deel acp --online 으로 띄우세요.');
259
345
  }
260
- allowEndpoint(conn.base);
346
+ // 제 주소만 말하면 옆 탭의 주소가 지워진다 — 연 주소 전부를 한 번에 (위 열린주소 머리말).
347
+ 열린주소.add(conn.base);
348
+ allowEndpoint([...열린주소]);
261
349
  if (실행모드.허가무시) setOffline(true);
262
350
 
263
351
  const session = new Session(conn, {
@@ -273,6 +361,30 @@ export async function acp(opts = {}) {
273
361
  // session 에 실어 두는 까닭은, 대화 도중 /model 로 옮겨도 같은 자리를 보게 하려는 것이다.
274
362
  session.실행모드 = 실행모드;
275
363
 
364
+ /*
365
+ * 열쇠를 받아 오는 명령 (safety/authcmd.js). oneshot.js 와 같은 까닭으로 건다 —
366
+ * 안 걸면 명령이 아무 말 없이 돌고 못 받은 까닭이 사라진다.
367
+ *
368
+ * 에디터의 승인 창은 **도구 호출**을 묻는 자리라 여기 쓰지 않는다. 무엇을
369
+ * 띄우는지 로그에 먼저 적고 띄운다. 명령은 이 PC 설정·관리 정책에서만 온다.
370
+ */
371
+ if (conn.열쇠받기) {
372
+ session.열쇠물어보기 = async (설정) => {
373
+ 로그(`열쇠를 받아 오는 명령을 띄웁니다: ${String(설정?.명령 ?? '').slice(0, 160)}`);
374
+ return true;
375
+ };
376
+ session.onAuth = (것) => {
377
+ if (것?.type === '시작') return 로그(옮긴말('auth.waiting'));
378
+ if (것?.type === '끝') return undefined;
379
+ if (것?.ok) {
380
+ const 분 = 것.만료 ? Math.max(0, Math.round((것.만료 - Date.now()) / 60000)) : '?';
381
+ return 로그(옮긴말('auth.got', { 분 }));
382
+ }
383
+ 로그(`${옮긴말('auth.failed', { 왜: 것?.왜 ?? '?' })}${것?.보인것 ? ` — ${String(것.보인것).slice(0, 160)}` : ''}`);
384
+ return undefined;
385
+ };
386
+ }
387
+
276
388
  const found = discover(root);
277
389
  session.skills = found.skills;
278
390
  session.commands = found.commands;
@@ -288,18 +400,53 @@ export async function acp(opts = {}) {
288
400
  */
289
401
  if (아이디) {
290
402
  const { messages: 적힌것 } = store.load();
291
- const { messages, 고친것 } = repairToolPairs(적힌것);
403
+ const { messages: 짝맞춘것, 고친것 } = repairToolPairs(적힌것);
404
+ /*
405
+ * 다른 규격으로 적힌 대화면 지금 규격으로 옮겨 적는다 (agent/session.js 의 규격맞추기).
406
+ * 저장 파일 머리글에는 규격이 없어서, 어제 Anthropic 으로 한 대화를 오늘 OpenAI
407
+ * 호환 프로필로 열면 tool_use 블록이 그대로 나가 첫 마디가 400 이었다.
408
+ * 터미널은 threads.js 가 같은 것을 한다.
409
+ */
410
+ const 옮김 = 규격맞추기(짝맞춘것, conn.kind);
411
+ const messages = 옮김.messages;
292
412
  session.messages = messages;
413
+ if (옮김.바꾼것) 로그(`${store.id} — 다른 규격으로 적힌 메시지 ${옮김.바꾼것}개를 ${conn.kind} 모양으로 옮겨 적었습니다.`);
293
414
  // 못 박아 둔 것도 같이 되살린다 (agent/pins.js). 이걸 빠뜨리면 '접어도
294
415
  // 안 지워진다' 가 에디터를 닫았다 여는 한 번에 거짓이 된다.
295
416
  const 박힌것 = store.못박은것읽기();
296
- if (박힌것.length) session.못박은것 = new 못박기(박힌것);
417
+ if (박힌것.length) {
418
+ session.못박은것 = new 못박기(박힌것);
419
+ // 다 안 실리면 적는다 (6회차 못박기6u W4 — repl.js 되살리기와 같은 까닭).
420
+ const 실림 = session.못박은것.실린것();
421
+ if (실림.개수 < 박힌것.length) 로그(`${store.id} — 못 박아 둔 것 ${박힌것.length}개 가운데 ${실림.개수}개만 프롬프트에 실립니다 (개수·자리 상한). /pin 으로 줄이세요.`);
422
+ }
297
423
  로그(`${store.id} — 메시지 ${messages.length}개를 이어 받았습니다${고친것 ? ` (끊긴 도구 호출 ${고친것}개는 걷어냈습니다)` : ''}.`);
298
424
  }
299
425
  // 주소는 가려서 적는다. 열쇠가 주소에 박혀 오는 게이트웨이가 있어서,
300
426
  // 그대로 적으면 대화 기록 파일에 열쇠가 남는다 (safety/secrets.js).
301
427
  store.begin({ model: conn.model, base: 주소가리기(conn.base), root });
302
- try { prune(root); } catch { /* 정리는 못 해도 대화는 된다 */ }
428
+ // begin() 이 이름을 옮겼을 수 있으니 그 뒤에 적는다. Store 를 지은 자리부터 여기까지
429
+ // 기다림이 없어서, 겹쳐 온 session/new 도 이 적힌 것을 보고 이름을 피한다.
430
+ 열린자리.set(store.id, root);
431
+ /*
432
+ * 남은 할 일과 시킨 말 원문도 이 파일과 묶는다 (agent/store.js 의 살림따라가기).
433
+ *
434
+ * 이걸 거는 자리가 터미널(agent/threads.js)에만 있었다. 그래서 에디터로 연 대화는
435
+ * todo·request 줄을 한 번도 안 적었고, session/load 로 되살려도 할 일이 안 돌아왔다 —
436
+ * store.js 가 「따로 부르는 자리를 만들면 그 길로 들어온 사람만 이어하기가 반쪽이
437
+ * 된다」 고 적어 둔 바로 그 모양이다. 이어하기면 여기서 할 일·시킨 말을 되살리고,
438
+ * 그 뒤로는 적기() 의 append 마다 바뀐 만큼 적힌다. 첫 한마디 전인 여기여야 한다.
439
+ */
440
+ store.살림따라가기(session);
441
+ /*
442
+ * 열어 둔 대화는 정리에서 뺀다 (사냥5 H5-2, agent/store.js 의 prune 머리말).
443
+ *
444
+ * 한 달 넘은 대화를 session/load 로 되살리면 그 파일은 시각이 옛날 그대로라, 바로
445
+ * 여기서 지워지고 다음 한 줄이 머리글도 옛 대화도 없는 새 파일에 적혔다. 같은
446
+ * 폴더에 탭을 하나 더 열어도 이 정리가 또 도므로, 이 방 하나가 아니라 **이
447
+ * 프로세스가 연 대화 전부**를 넘긴다. 다른 폴더의 이름이 섞여도 잃는 것은 없다.
448
+ */
449
+ try { prune(root, { 남길것: [...열린자리.keys()] }); } catch { /* 정리는 못 해도 대화는 된다 */ }
303
450
 
304
451
  /*
305
452
  * 밖에서 붙인 도구(MCP).
@@ -434,6 +581,20 @@ export async function acp(opts = {}) {
434
581
  // 안 붙는 것의 차이가 크다 — 안 붙으면 위험한 명령을 물어볼 데가 없어서
435
582
  // 무조건 거부하게 되고, 그러면 에디터 안에서는 아무 일도 못 하는 도구가 된다.
436
583
  async function 승인묻기(방, 이름, 인자) {
584
+ /*
585
+ * ── 끊긴 턴의 승인은 없다 (사냥5 H5-3) ─────────────────────────────────
586
+ *
587
+ * 승인 창을 띄워 두고 사람이 안 누른 채 취소했다. 여기 기다림이 턴의 끊기 신호를
588
+ * 안 봐서, 그 턴은 취소된 뒤에도 **답이 올 때까지** 서 있었다. 한참 뒤 옛 창의
589
+ * 「이번만 실행」 이 닿자 취소한 턴이 그대로 파일을 썼다. 사람은 취소를 눌렀는데.
590
+ *
591
+ * 그래서 기다림을 지금 도는 턴의 신호에 묶고(끊기면 거둔다), 답이 온 **뒤에도**
592
+ * 한 번 더 본다 — 답과 취소가 엇갈려 닿으면 허락이 이긴 채로 돌아갈 수 있다.
593
+ * 신호는 방.도는신호 에서 읽는다. 한 방의 턴은 앞 턴이 끝나야 도므로(한턴 머리말)
594
+ * 이 물음을 부른 턴이 곧 그 신호의 턴이다.
595
+ */
596
+ const 신호 = 방.도는신호 ?? null;
597
+ if (신호?.aborted) return false;
437
598
  if (방.늘허락.has(이름)) return true;
438
599
 
439
600
  const 아이디 = `t${++방.도구번호}`;
@@ -453,8 +614,10 @@ export async function acp(opts = {}) {
453
614
  { optionId: 'allow_always', name: `${이름} 은 앞으로 묻지 않기`, kind: 'allow_always' },
454
615
  { optionId: 'reject_once', name: '하지 않기', kind: 'reject_once' },
455
616
  ],
456
- });
617
+ }, { signal: 신호 });
457
618
 
619
+ // 답을 기다리는 사이에 턴이 끊겼으면 허락이 와도 안 한다 (위 머리말).
620
+ if (신호?.aborted) return false;
458
621
  const 결과 = 답?.outcome ?? {};
459
622
  if (결과.outcome !== 'selected') return false; // cancelled 도 여기로 온다
460
623
  if (결과.optionId === 'allow_always') {
@@ -470,7 +633,9 @@ export async function acp(opts = {}) {
470
633
  * 허락으로 보여서, 사람이 풀린 줄 안다.
471
634
  */
472
635
  try {
473
- const cfg = load();
636
+ // 이 방의 폴더로 읽는다. 그냥 load() 는 에디터를 띄운 자리의 설정과
637
+ // 금지를 봐서, 열어 둔 프로젝트의 deny 와 부딪치는지를 엉뚱하게 갈랐다.
638
+ const cfg = load({ root: 방.root });
474
639
  const r = 늘허락(cfg, 이름, 규칙모으기(cfg));
475
640
  if (r.ok) saveCfg(cfg);
476
641
  } catch { /* 못 적어도 이번 켠 동안은 먹는다 */ }
@@ -478,6 +643,11 @@ export async function acp(opts = {}) {
478
643
  }
479
644
  return 결과.optionId === 'allow_once';
480
645
  } catch (err) {
646
+ // 턴이 끊겨 기다림을 거둔 것이면 「못 물어봤다」 가 아니다. 그렇게 적으면 로그를 읽는 사람이 에디터 탓을 한다.
647
+ if (신호?.aborted || err?.거둠) {
648
+ 로그(`턴이 끊겨 승인 기다림을 거뒀습니다 (${이름}) — 하지 않습니다.`);
649
+ return false;
650
+ }
481
651
  /*
482
652
  * 못 물어봤으면 안 한다.
483
653
  *
@@ -499,10 +669,42 @@ export async function acp(opts = {}) {
499
669
  // 앞 턴이 아직 돌고 있으면 끊고 시작한다. 규격은 턴을 겹쳐 보내지 말라고
500
670
  // 하지만, 안 지키는 클라이언트가 있을 때 두 턴이 같은 세션을 같이 밟으면
501
671
  // 오간 말이 뒤엉킨다. 그건 나중에 원인을 찾을 수 없는 종류의 고장이다.
502
- if (방.턴 && !방.턴.signal.aborted) 방.턴.abort();
503
-
672
+ /*
673
+ * ── 끊기만 하고 안 기다렸다 (사냥5 H5-3) ───────────────────────────────
674
+ *
675
+ * abort() 는 「그만하라」 는 말이지 「그만뒀다」 가 아니다. 앞 턴은 승인 답이나
676
+ * 도구가 끝나기를 기다리는 중일 수 있고, 그 사이에 새 턴을 바로 시작하면 두 턴이
677
+ * 한 대화를 같이 밟았다 — 대화 파일에 새 턴의 말이 두 번 적히고, 앞 턴의 도구
678
+ * 결과가 제 부름에서 떨어진 자리에 적혔다(되살리면 규격 서버가 400 을 준다).
679
+ *
680
+ * 그래서 끊은 뒤 **앞 턴이 적기까지 마치고 물러날 때까지** 기다린다. 앞 턴의
681
+ * 기다림(승인·모델)은 끊기 신호에 묶여 있어 곧 끝난다. 기다리는 사이에 또 새
682
+ * 말이 오면 이 턴도 끊기므로, 끊는 줄(방.턴)은 여기서 바로 넘기고 차례(방.턴끝)는
683
+ * 사슬로 잇는다 — 마지막에 온 말만 돈다.
684
+ */
504
685
  const 턴 = new AbortController();
686
+ const 앞턴 = 방.턴;
505
687
  방.턴 = 턴;
688
+ if (앞턴 && !앞턴.signal.aborted) 앞턴.abort();
689
+ const 앞끝 = 방.턴끝;
690
+ let 끝알림 = () => {};
691
+ const 이번끝 = new Promise((풀기) => { 끝알림 = 풀기; });
692
+ 방.턴끝 = 이번끝;
693
+ try {
694
+ if (앞끝) await 앞끝;
695
+ if (턴.signal.aborted) return { stopReason: 멈춘까닭('aborted') };
696
+ return await 턴돌리기(방, 글, 턴);
697
+ } finally {
698
+ if (방.턴 === 턴) 방.턴 = null;
699
+ if (방.턴끝 === 이번끝) 방.턴끝 = null;
700
+ 끝알림();
701
+ }
702
+ }
703
+
704
+ /** 한 턴의 몸통. 차례를 받은 뒤에만 부른다 (위 한턴). */
705
+ async function 턴돌리기(방, 글, 턴) {
706
+ // 승인 물음이 이 턴의 끊기 신호를 보게 한다 (승인묻기 머리말, 사냥5 H5-3).
707
+ 방.도는신호 = 턴.signal;
506
708
  방.ctx.카드다시();
507
709
 
508
710
  const 보내기 = (update) => 관.알림('session/update', { sessionId: 방.id, update });
@@ -512,11 +714,40 @@ export async function acp(opts = {}) {
512
714
  messageId: `m${방.메시지번호}`,
513
715
  });
514
716
 
515
- // 종합 모드면 이 한마디를 보고 알맞은 작업 모드로 옮긴다. 대화 화면과 같다.
717
+ /*
718
+ * 종합 모드면 이 한마디를 보고 알맞은 작업 모드로 옮긴다.
719
+ *
720
+ * ── 옮기고 아무 말도 안 했다 ──────────────────────────────────────
721
+ *
722
+ * 대화 화면과 `deel run` 은 어느 모드로 갔는지, 무슨 말 때문인지 찍는다.
723
+ * 여기만 조용히 옮겼다. 에디터 쪽 사람은 **파일을 못 고치는 모드**로
724
+ * 바뀐 것을 모른 채 「왜 안 고쳐?」 를 본다. 모드는 이 턴에 파일이
725
+ * 바뀌는지를 정하는 값이라, 안 말하면 안 되는 값이다.
726
+ *
727
+ * ── 여기서도 승인받을 자리가 없다 ─────────────────────────────────
728
+ *
729
+ * 겹친 요청을 계획 모드로 보내는 값은 「계획을 보여 주고 승인을 받아
730
+ * 그대로 잇는다」 인데, 이어 가는 길(`이어갈모드`)은 repl.js 에만 있다.
731
+ * 에디터에서 사람이 제일 자연스럽게 쓰는 승인말이 되레 갇힌다 —
732
+ * 「위 계획대로 진행해줘」 에는 '계획' 이 들어 있어서 route 가 또 계획
733
+ * 모드를 고른다. 계획을 두 장 받고 파일은 그대로다.
734
+ *
735
+ * 이을 수 없으면 계획부터 내지 않는다. 그리고 그렇게 한다고 말한다.
736
+ */
516
737
  방.session.routed = null;
517
738
  if (방.session.work === 'auto') {
518
- const 골라진 = route(글);
519
- if (골라진.mode) 방.session.routed = 골라진.mode;
739
+ const 골라진 = route(글, { 승인받을수있나: false });
740
+ if (골라진.mode) {
741
+ 방.session.routed = 골라진.mode;
742
+ 말하기(`◆ ${보일이름(골라진.mode)} — 말 속에 ${골라진.why} 가 있어서\n`);
743
+ } else if (골라진.일부러 && !골라진.겹침) {
744
+ // 겹쳤을 때는 안 찍는다 — 바로 아래가 같은 이야기를 더 온전히 한다.
745
+ 말하기(`◇ 종합 그대로 — ${골라진.why}\n`);
746
+ }
747
+ if (골라진.겹침) {
748
+ 말하기('◇ 계획과 실행이 같이 있지만 에디터에서는 승인받고 이어 갈 길이 없어'
749
+ + ' 한 번에 끝까지 합니다. 계획을 먼저 보시려면 터미널 대화 화면에서 하세요.\n');
750
+ }
520
751
  }
521
752
 
522
753
  let 까닭 = 'done';
@@ -534,6 +765,24 @@ export async function acp(opts = {}) {
534
765
  const 적기 = () => {
535
766
  for (const m of 방.session.messages.slice(적은데까지)) 방.store.append(m);
536
767
  적은데까지 = 방.session.messages.length;
768
+ 저장샘알리기();
769
+ };
770
+ /*
771
+ * ── 못 적은 것을 아무에게도 안 말했다 (사냥5 H5-6) ───────────────────────
772
+ *
773
+ * 에디터 창 둘이 같은 대화를 되살리면, 뒤에 적는 쪽은 두 대화를 섞지 않으려고
774
+ * **안 적고 센다**(agent/store.js 의 OTHER_WINDOW). 디스크가 차거나 권한이 막혀도
775
+ * 같은 셈에 오른다. 대화 화면은 그 셈을 한 번 말해 주는데(repl.js 의 저장샘) 여기는
776
+ * store.못쓴것 을 한 번도 안 봤다. 둘째 창에서 한 말은 조용히 버려졌고, 사람은
777
+ * 다음에 되살릴 때에야 그 말이 없다는 것을 알았다 — 이미 늦은 자리다.
778
+ * 처음 한 번만 말한다(처음못쓴것). 로그와 에디터 화면 둘 다에.
779
+ */
780
+ const 저장샘알리기 = () => {
781
+ const 샘 = 방.store.처음못쓴것?.();
782
+ if (!샘) return;
783
+ const 알림글 = 옮긴말('store.notSaving', { n: 샘.수, 까닭: 샘.까닭 });
784
+ 로그(`${방.id} — ${알림글}`);
785
+ try { 말하기(`\n\n_(${알림글})_\n\n`); } catch { /* 관이 닫혔으면 로그만 남는다 */ }
537
786
  };
538
787
 
539
788
  try {
@@ -673,10 +922,13 @@ export async function acp(opts = {}) {
673
922
  : 옮긴말('ev.resetBare', { 버린수: ev.dropped })})_\n\n`);
674
923
  방.store.replace(방.session.messages, `자리부족 — 앞선 대화 ${ev.dropped}개를 비움`);
675
924
  적은데까지 = 방.session.messages.length;
925
+ 저장샘알리기();
676
926
  break;
677
927
  case 'compacted':
678
- 방.store.replace(방.session.messages, `압축 — ${ev.folded}개를 요약으로`);
928
+ 방.store.replace(방.session.messages,
929
+ ev.fallback ? `압축 못 함 — 옛 대화 ${ev.folded}개를 잘라 냄` : `압축 — ${ev.folded}개를 요약으로`);
679
930
  적은데까지 = 방.session.messages.length;
931
+ 저장샘알리기();
680
932
  break;
681
933
 
682
934
  /*
@@ -745,6 +997,7 @@ export async function acp(opts = {}) {
745
997
  왜 = String(err?.message ?? err);
746
998
  } finally {
747
999
  if (방.턴 === 턴) 방.턴 = null;
1000
+ if (방.도는신호 === 턴.signal) 방.도는신호 = null;
748
1001
  방.돌던도구.clear();
749
1002
  // 끊겼든 터졌든 여기까지 오간 것은 남긴다. 끊긴 턴이야말로 다음에
750
1003
  // 되살려서 이어가고 싶은 자리다.
@@ -825,9 +1078,17 @@ export async function acp(opts = {}) {
825
1078
  * 열고, 그 다음 한마디에서야 같은 오류가 난다. 한 번 더 돌아가는 셈이다.
826
1079
  */
827
1080
  case 'authenticate': {
1081
+ /*
1082
+ * 빈 methodId 도 **모르는 방법**이다 (8회차 ACP-5).
1083
+ *
1084
+ * `골라온것 &&` 로 시작하던 때는 빈 글이 검사 자체를 건너뛰어 그냥 성공으로
1085
+ * 돌아갔다 — jsonrpc.js 의 빈 method 와 같은 꼴이다. methodId 는 규격이 반드시
1086
+ * 주라고 한 칸이고, 빈 글은 위 인증방법들 중 어느 것도 아니다. 성공이라 답하면
1087
+ * 에디터는 있지도 않은 방법으로 인증이 끝난 줄 알고 그 뒤를 잇는다.
1088
+ */
828
1089
  const 골라온것 = String(인자?.methodId ?? '');
829
- if (골라온것 && !인증방법들.some((m) => m.id === 골라온것)) {
830
- throw 잘못된인자오류(`모르는 인증 방법입니다: ${골라온것}`);
1090
+ if (!인증방법들.some((m) => m.id === 골라온것)) {
1091
+ throw 잘못된인자오류(`모르는 인증 방법입니다: ${골라온것 || '(빈 칸)'} (쓸 수 있는 것: ${인증방법들.map((m) => m.id).join(', ')})`);
831
1092
  }
832
1093
  if (!activeProfile(load())) {
833
1094
  throw 인증필요오류('아직 설정이 안 끝났습니다. 터미널에서 `deel setup` 을 마치고 다시 시도하세요.');
@@ -859,10 +1120,22 @@ export async function acp(opts = {}) {
859
1120
  case 'session/load': {
860
1121
  const 아이디 = String(인자?.sessionId ?? '').trim();
861
1122
  if (!아이디) throw 잘못된인자오류('sessionId 가 없습니다.');
1123
+ /*
1124
+ * 이름이 곧 파일 경로의 한 조각이다. `../../evil/x` 를 그대로 받으면 대화 폴더
1125
+ * 밖의 파일을 대화로 읽고, 한 턴이 돌면 거기에 대화를 적었다. Store 도 막지만
1126
+ * 그건 조용히 빈 대화로 여는 것이라, 에디터에는 **잘못된 인자**로 또렷하게 답한다.
1127
+ */
1128
+ if (!대화이름인가(아이디)) throw 잘못된인자오류(`대화 이름이 아닙니다: ${아이디}`);
862
1129
 
863
1130
  // 이 프로세스가 이미 열어 둔 방이면 그대로 쓴다. 다시 만들면 같은
864
1131
  // 대화를 두 곳에서 밟게 되고, 그때부터 어느 쪽이 참인지 알 수 없다.
865
- const 방 = 방들.get(아이디) ?? await 방만들기(인자, { 아이디 });
1132
+ //
1133
+ // 단 **같은 폴더**일 때만이다 (사냥5 H5-1). 이름만 보고 돌려주면 B 폴더의 그
1134
+ // 이름 대화를 되살리라는데 A 폴더 방이 나가서, 그 뒤 한 말이 A 에 적히고 A 를
1135
+ // 고친다. 폴더가 다르면 방만들기 로 보내고, 거기서 또렷하게 거절한다.
1136
+ const 있던방 = 방들.get(아이디);
1137
+ const 바란자리 = typeof 인자?.cwd === 'string' && 인자.cwd ? 인자.cwd : (opts.root ?? process.cwd());
1138
+ const 방 = 있던방 && 같은자리(있던방.root, 바란자리) ? 있던방 : await 방만들기(인자, { 아이디 });
866
1139
 
867
1140
  /*
868
1141
  * 화면에 그릴 것과 모델에게 줄 것이 다르다.
@@ -904,7 +1177,16 @@ export async function acp(opts = {}) {
904
1177
 
905
1178
  case 'session/set_mode': {
906
1179
  const 방 = 방찾기(인자?.sessionId);
907
- const 고른것 = 모드정리(String(인자?.modeId ?? ''));
1180
+ const 준것 = String(인자?.modeId ?? '');
1181
+ const 고른것 = 모드정리(준것);
1182
+ /*
1183
+ * 모르는 이름은 **잘못된 인자**다.
1184
+ *
1185
+ * 정리하면 null 이 나오는 이름을 그대로 넣었다. 답은 `{}` (성공), 모드는
1186
+ * null(종합으로 떨어짐), 로그는 「작업 모드를 null 로 바꿨습니다」. 에디터는
1187
+ * 누른 단추가 먹힌 줄 알고 그 모드로 칠해 둔다 — 화면과 실제가 어긋난다.
1188
+ */
1189
+ if (!고른것) throw 잘못된인자오류(`그런 작업 모드가 없습니다: ${준것} (쓸 수 있는 것: ${모드순서.join(', ')})`);
908
1190
  방.session.work = 고른것;
909
1191
  방.session.routed = null;
910
1192
  로그(`작업 모드를 ${고른것} 로 바꿨습니다.`);
@@ -940,6 +1222,18 @@ export async function acp(opts = {}) {
940
1222
 
941
1223
  await new Promise((끝) => {
942
1224
  const 마무리 = () => {
1225
+ /*
1226
+ * ── 돌던 턴부터 끊는다 (사냥5 H5-8) ─────────────────────────────────
1227
+ *
1228
+ * 여기는 관만 닫았다. 느린 모델을 기다리던 턴은 끊는 줄을 못 받아 **답이 올
1229
+ * 때까지** 살아 있었고, 프로세스도 그만큼 안 끝났다(재 보니 모델이 15초
1230
+ * 걸리면 13초 넘게). 에디터는 이미 닫혔는데 그 뒤에 온 답이 대화 파일에
1231
+ * 적혔다 — 아무도 못 본 답이다. 관을 닫기 전에 끊어야 기다리던 승인 물음도
1232
+ * 「끊겨서 거뒀다」 로 정리된다.
1233
+ */
1234
+ for (const 방 of 방들.values()) {
1235
+ if (방.턴 && !방.턴.signal.aborted) 방.턴.abort();
1236
+ }
943
1237
  관.닫기('에디터와의 관이 닫혔습니다');
944
1238
  끝();
945
1239
  };
@@ -948,6 +1242,20 @@ export async function acp(opts = {}) {
948
1242
  process.stdin.on('error', 마무리);
949
1243
  });
950
1244
 
1245
+ /*
1246
+ * 끊은 턴이 여기까지 오간 것을 적고 물러날 때까지 **잠깐만** 기다린다. 끊긴 턴이야말로
1247
+ * 다음에 되살려 잇고 싶은 자리라(한턴 의 적기) 안 기다리고 나가면 그게 빠진다.
1248
+ * 끊기 신호를 안 듣는 도구가 붙들고 있으면 3초에서 그만둔다 — 닫힌 에디터 앞에서
1249
+ * 서 있는 것이 고치려던 고장이다.
1250
+ */
1251
+ const 끝날턴들 = [...방들.values()].map((방) => 방.턴끝).filter(Boolean);
1252
+ if (끝날턴들.length) {
1253
+ await Promise.race([
1254
+ Promise.allSettled(끝날턴들),
1255
+ new Promise((풀기) => { setTimeout(풀기, 3000).unref?.(); }),
1256
+ ]);
1257
+ }
1258
+
951
1259
  /*
952
1260
  * 뒤에서 돌던 명령을 반드시 거둔다.
953
1261
  *