deel-local-cli 1.17.4 → 1.17.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md CHANGED
@@ -20,7 +20,7 @@
20
20
 
21
21
  [![Node.js CI](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/test.yml?branch=main&logo=github&logoColor=white&label=Node.js%20CI)](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
22
22
  [![CodeQL](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/codeql.yml?branch=main&logo=github&logoColor=white&label=CodeQL)](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
23
- [![tests](https://img.shields.io/badge/tests-7%2C937%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/ko/develop.md)
23
+ [![tests](https://img.shields.io/badge/tests-7%2C991%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)
@@ -268,7 +268,7 @@ deel --offline
268
268
  무엇이 어디로 갈 수 있는지는 켤 때 화면 맨 위에 늘 적혀 있습니다.
269
269
 
270
270
  ```
271
- deel 1.17.4 ⌂ 이 안
271
+ deel 1.17.5 ⌂ 이 안
272
272
  보냄 이 컴퓨터 안 127.0.0.1:11434 ← 여기 말고는 어디로도 안 갑니다
273
273
  ```
274
274
 
@@ -1185,7 +1185,7 @@ deel stats # 이 폴더에서 실제로 무엇을 했나 (.de
1185
1185
  ## 개발
1186
1186
 
1187
1187
  ```bash
1188
- npm test 전체 검증 (7,937항목 — 몇몇은 터미널에 따라 갈립니다)
1188
+ npm test 전체 검증 (7,991항목 — 몇몇은 터미널에 따라 갈립니다)
1189
1189
  npm run coverage 검사가 소스의 어디를 밟았는지
1190
1190
  npm run verify 반입·통신 검증만
1191
1191
  npm run bench 편집 성공률 측정
@@ -1254,6 +1254,7 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
1254
1254
 
1255
1255
  | 판 | 무엇이 바뀌었나 |
1256
1256
  |---|---|
1257
+ | [1.17.5](docs/ko/releases/1.17.md#1175) | 한 턴 안에서 단계가 일을 따라갑니다 — 그리고 읽기 도구 넷이 여태 줄을 서 있었습니다 |
1257
1258
  | [1.17.4](docs/ko/releases/1.17.md#1174) | 같은 요청을 한 번 더 보내던 자리들 — 그리고 배운 것이 스스로를 봉인하던 자리 |
1258
1259
  | [1.17.3](docs/ko/releases/1.17.md#1173) | 「같은 대화다」 를 적는 표준 이름이 없습니다 — 아는 이름을 다 답니다 |
1259
1260
  | [1.17.2](docs/ko/releases/1.17.md#1172) | 다른 눈으로 한 번 더 — 한글이 못 여는 hwpx · 이름 없이 나가던 요청 둘 |
package/README.md CHANGED
@@ -20,7 +20,7 @@ Vendor APIs connect too — **only when you say so**
20
20
 
21
21
  [![Node.js CI](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/test.yml?branch=main&logo=github&logoColor=white&label=Node.js%20CI)](https://github.com/jysvai/deel-local-cli/actions/workflows/test.yml)
22
22
  [![CodeQL](https://img.shields.io/github/actions/workflow/status/jysvai/deel-local-cli/codeql.yml?branch=main&logo=github&logoColor=white&label=CodeQL)](https://github.com/jysvai/deel-local-cli/actions/workflows/codeql.yml)
23
- [![tests](https://img.shields.io/badge/tests-7%2C937%20passing-1a7f37?logo=checkmarx&logoColor=white)](docs/en/develop.md)
23
+ [![tests](https://img.shields.io/badge/tests-7%2C991%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)
@@ -339,7 +339,7 @@ deel --offline
339
339
  The destination is printed at the top of every session:
340
340
 
341
341
  ```
342
- deel 1.17.4 ⌂ inside
342
+ deel 1.17.5 ⌂ inside
343
343
  Sends to this machine 127.0.0.1:11434 ← nowhere else
344
344
  ```
345
345
 
@@ -1258,7 +1258,7 @@ Stored in `~/.deel/config.json`. A `.deel/config.json` in the project folder tak
1258
1258
  ## Development
1259
1259
 
1260
1260
  ```bash
1261
- npm test Full suite (7,937 checks; a few are TTY-dependent)
1261
+ npm test Full suite (7,991 checks; a few are TTY-dependent)
1262
1262
  npm run coverage Which lines the tests actually execute
1263
1263
  npm run verify Import + network checks only
1264
1264
  npm run bench Edit success rate
@@ -1313,6 +1313,7 @@ so one run tells you everything.
1313
1313
 
1314
1314
  | Version | What changed |
1315
1315
  |---|---|
1316
+ | [1.17.5](docs/en/releases/1.17.md#1175) | The phase now follows the work inside a turn — and four read tools had been queueing up |
1316
1317
  | [1.17.4](docs/en/releases/1.17.md#1174) | The places that sent the same request twice — and where what it learned sealed itself in |
1317
1318
  | [1.17.3](docs/en/releases/1.17.md#1173) | No standard name for "same conversation" — it sends every name it knows |
1318
1319
  | [1.17.2](docs/en/releases/1.17.md#1172) | A second pair of eyes — an hwpx Hancom cannot open · two requests going out unnamed |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deel-local-cli",
3
- "version": "1.17.4",
3
+ "version": "1.17.5",
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",
package/src/agent/loop.js CHANGED
@@ -20,6 +20,9 @@ import { allowTemporarily, isOffline } from '../safety/network.js';
20
20
  import { 가리기, 훑기, 가렸다는말, 봤다는말, 가릴까 } from '../safety/secrets.js';
21
21
  import { 바깥인가 } from '../safety/runmode.js';
22
22
  import { get as workMode } from './modes.js';
23
+ // 종합 모드에서 단계가 일을 따라간다 (agent/단계.js).
24
+ import { 다음단계 } from './단계.js';
25
+ import { 묻지말라했나, 손대라했나 } from './route.js';
23
26
  import { 지시말 } from '../i18n/index.js';
24
27
  import { 빠진것, 빠졌다는말 } from './asks.js';
25
28
  import { 환경속열쇠들 } from '../config.js';
@@ -102,14 +105,31 @@ function thinkFor(conn, level) {
102
105
  return level;
103
106
  }
104
107
 
105
- // 아무것도 안 바꾸는 도구들. 이것들만 동시에 돌린다.
106
- //
107
- // 왜 이것만인가:
108
- // Read 개를 동시에 하는 것은 안전하다 — 서로 안 건드린다.
109
- // Write·Edit 동시에 돌리면 같은 파일을 갈래로 고칠 수 있고,
110
- // 되돌리기 스냅샷 순서도 뒤엉킨다. Bash 무슨 짓을 할지 없다.
111
- // 그래서 '읽기만 하는 것' 이라고 확실한 도구만 묶는다.
112
- const 읽기전용 = new Set(['Read', 'Glob', 'Grep', 'Skill', 'WebFetch']);
108
+ /*
109
+ * 아무것도 안 바꾸는 도구들. **이것들만** 동시에 돌린다.
110
+ *
111
+ * 이것만인가:
112
+ * Read 개를 동시에 하는 것은 안전하다 서로 건드린다.
113
+ * Write·Edit 동시에 돌리면 같은 파일을 갈래로 고칠있고,
114
+ * 되돌리기 스냅샷 순서도 뒤엉킨다. Bash 무슨 짓을 할지 알 수 없다.
115
+ * 그래서 '읽기만 하는 ' 이라고 확실한 도구만 묶는다.
116
+ *
117
+ * ── 그런데 이 목록이 modes.js 의 읽기 갈래보다 **짧았다** ───────────────
118
+ *
119
+ * modes.js 는 읽기만 하는 도구를 열 개로 세는데(Read·Outline·Glob·Grep·Def·
120
+ * Refs·WebFetch·Skill·Recall·Ask), 여기는 다섯 개만 알고 있었다. 나머지는
121
+ * 「안전한지 모르겠다」 가 아니라 그냥 **빠져 있었다.**
122
+ *
123
+ * 값이 큰 자리다. 시킴말이 남의 코드를 볼 때 **제일 먼저 부르라고 적어 둔
124
+ * 도구가 Outline** 이고(modes.js 의 읽는법), 그게 여태 한 개씩 돌았다.
125
+ * Def·Refs 는 언어 서버 왕복이라 기다리는 시간이 곧 사람이 기다리는 시간인데
126
+ * 그것도 한 줄로 섰다. 파일 여섯 개의 모양을 보는 데 여섯 번을 왕복했다.
127
+ *
128
+ * Ask 만 뺀다. 그건 사람에게 묻는 도구다 — 셋을 한꺼번에 물으면 화면에
129
+ * 물음 셋이 겹쳐 뜨고, 어느 답이 어느 물음의 것인지 사람이 못 가른다.
130
+ * 읽기 전용인 것은 맞지만 **사람의 차례를 쓰는 도구**라 줄을 세워야 한다.
131
+ */
132
+ const 읽기전용 = new Set(['Read', 'Outline', 'Glob', 'Grep', 'Def', 'Refs', 'Recall', 'Skill', 'WebFetch']);
113
133
 
114
134
  /*
115
135
  * 잘린 답을 「생각을 줄여」 다시 부를 때 쓰는 두 값.
@@ -414,7 +434,7 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
414
434
  * 여전히 설계 모드라고 떠 있는 채로 파일이 바뀐다. task.js 의 하위모드() 가
415
435
  * 모드 쪽에서 한 겹 막고, 여기가 도구 쪽에서 한 겹 더 막는다.
416
436
  */
417
- const tools = toolSchemas(session.도구제한 ?? null, {
437
+ let tools = toolSchemas(session.도구제한 ?? null, {
418
438
  hasSkills: (session.skills?.length ?? 0) > 0,
419
439
  web: session.web !== false && !isOffline(), // 오프라인이면 웹 도구는 아예 안 보여 준다
420
440
  work: session.effectiveWork(), // 작업 모드가 쓰는 것만 (modes.js)
@@ -440,17 +460,17 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
440
460
  * 이름은 그 표에 없어서 undefined 를 읽다 죽는다. MCP 는 이름 목록이 아니라
441
461
  * ctx.mcp 로 따로 넘어가므로 하위도 그 길로 똑같이 받는다.
442
462
  */
443
- const 내도구 = tools.map((t) => t.function.name).filter((n) => !n.startsWith('mcp__'));
463
+ let 내도구 = tools.map((t) => t.function.name).filter((n) => !n.startsWith('mcp__'));
444
464
  /*
445
465
  * 깊이 상한. 부모(0) → 하위(1) → 하위의 하위(2) 까지다.
446
466
  *
447
467
  * 상한에 닿으면 목록에서 Task 를 뺀다. "더 쪼개지 마라" 고 부탁하지 않는다 —
448
468
  * 모델은 부탁을 잊고, 잊으면 하위가 하위를 끝없이 낳는다.
449
469
  */
450
- const 자식도구 = 깊이 + 1 >= 최대깊이 ? 내도구.filter((n) => n !== 'Task') : 내도구;
470
+ let 자식도구 = 깊이 + 1 >= 최대깊이 ? 내도구.filter((n) => n !== 'Task') : 내도구;
451
471
  // 모드마다 생각의 배분과 걸음 수가 다르다. 사용자가 따로 정했으면 그걸 존중한다.
452
- const 모드 = workMode(session.effectiveWork());
453
- const effort = session.effortSet ? session.effort : (모드.effort ?? session.effort);
472
+ let 모드 = workMode(session.effectiveWork());
473
+ let effort = session.effortSet ? session.effort : (모드.effort ?? session.effort);
454
474
  /*
455
475
  * ── 강도는 **천장**이고, 이번 턴 값은 시킨 말이 정한다 (agent/effort.js) ──
456
476
  *
@@ -472,8 +492,8 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
472
492
  * 그래서 파일 열 개를 읽고 경합 조건을 짚어야 하는 일까지 low 로 돌았다 —
473
493
  * 생각 블록이 0자로 나가고, 답이 그만큼 얕았다.
474
494
  */
475
- const 천장 = session.thinkSet ? session.think : 천장고르기(모드.think, session.think, userText);
476
- const think = 자동강도(userText, 천장, {
495
+ let 천장 = session.thinkSet ? session.think : 천장고르기(모드.think, session.think, userText);
496
+ let think = 자동강도(userText, 천장, {
477
497
  대화크기: session.messages.length,
478
498
  켜짐: session.autoThink !== false,
479
499
  });
@@ -492,7 +512,92 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
492
512
  * 없다 — 8k 모델에는 너무 크고, 655k 모델은 여유가 96% 남았는데도 만들다
493
513
  * 만 채로 끊겼다. 사람이 직접 준 값이 있으면 그것이 먼저다.
494
514
  */
495
- const maxSteps = session.stepsSet ? session.maxSteps : 걸음수(모드.id, conn.ctx);
515
+ let maxSteps = session.stepsSet ? session.maxSteps : 걸음수(모드.id, conn.ctx);
516
+ /*
517
+ * ── 단계가 일을 따라간다 (agent/단계.js) ────────────────────────────────
518
+ *
519
+ * 종합 모드는 한마디를 보고 단계를 고르는데, 그 고르기가 **턴이 시작할 때 딱
520
+ * 한 번** 돌았다. 마흔 걸음짜리 턴에서 일의 성격은 몇 번씩 바뀌는데, 시킴말은
521
+ * 첫 걸음의 것 그대로였다 — 원인을 다 찾고 고치는 중인데도 화면과 시킴말은
522
+ * 끝까지 「디버그」 였다.
523
+ *
524
+ * 여기서 세는 것은 **이미 일어난 일**이다. 모델에게 「지금 무슨 단계냐」 고
525
+ * 한 번 더 묻지 않는다. 그러면 걸음마다 왕복이 하나 늘고, 그 판단이 더
526
+ * 나을 까닭도 없다.
527
+ */
528
+ const 이번턴본것 = { 할일: false, 바꿈: false };
529
+ const 묻지말라 = 묻지말라했나(userText);
530
+ // 계획에서 넘어갈 때만 본다. 「묻지 마라」 는 **어떻게**의 말이고,
531
+ // 「고쳐라」 는 **무엇을**의 말이다. 범위를 정하는 것은 뒤엣것이다(단계.js).
532
+ const 손대라 = 손대라했나(userText);
533
+ const 종합인가 = session.work === 'auto';
534
+
535
+ /**
536
+ * 단계를 옮겨야 하면 옮기고, 옮겼으면 알린다.
537
+ *
538
+ * 시킴말은 저절로 따라온다 — session.wire() 가 effectiveWork() 를 매번 보고
539
+ * 짓기 때문이다(agent/session.js). 여기서 다시 지어야 하는 것은 **도구와
540
+ * 걸음 수와 생각의 배분**이다.
541
+ *
542
+ * @returns {object|null} 알릴 것이 있으면 이벤트, 없으면 null
543
+ */
544
+ const 단계옮기기 = () => {
545
+ const 갈곳 = 다음단계({ 지금: 모드.id, 종합인가, 본것: 이번턴본것, 묻지말라, 손대라 });
546
+ if (!갈곳) return null;
547
+ const 옛모드 = 모드.id;
548
+ session.routed = 갈곳;
549
+ 모드 = workMode(갈곳);
550
+
551
+ /*
552
+ * 도구가 늘면 **앞머리가 바뀐다.** 규격이 tools 를 맨 앞에 렌더하므로,
553
+ * 도구 하나가 늘어도 그 턴의 캐시는 거기서 끊긴다. 값이 드는 일이라
554
+ * 조용히 하지 않는다 — 화면에 적는다.
555
+ */
556
+ const 옛도구 = new Set(내도구);
557
+ tools = toolSchemas(session.도구제한 ?? null, {
558
+ hasSkills: (session.skills?.length ?? 0) > 0,
559
+ web: session.web !== false && !isOffline(),
560
+ work: session.effectiveWork(),
561
+ mcp: ctx.mcp ?? null,
562
+ 에이전트들: 깊이 + 1 >= 최대깊이 ? null : (ctx.에이전트들 ?? null),
563
+ lsp: session.lsp === true,
564
+ ctx: conn.ctx ?? null,
565
+ vision: conn.vision === true,
566
+ });
567
+ 내도구 = tools.map((t) => t.function.name).filter((n) => !n.startsWith('mcp__'));
568
+ 자식도구 = 깊이 + 1 >= 최대깊이 ? 내도구.filter((n) => n !== 'Task') : 내도구;
569
+ const 늘어난것 = 내도구.filter((n) => !옛도구.has(n));
570
+
571
+ // 생각의 배분도 그 단계 것으로. 사람이 직접 정했으면 그것이 먼저다.
572
+ if (!session.effortSet) effort = 모드.effort ?? session.effort;
573
+ if (!session.thinkSet) {
574
+ 천장 = 천장고르기(모드.think, session.think, userText);
575
+ think = 자동강도(userText, 천장, {
576
+ 대화크기: session.messages.length,
577
+ 켜짐: session.autoThink !== false,
578
+ });
579
+ }
580
+ /*
581
+ * 걸음 수는 **늘리기만** 한다.
582
+ *
583
+ * 계획 모드의 상한은 코드 모드보다 작다. 넘어가면서 그 작은 값을 그대로
584
+ * 들고 가면 만들다 말고 끊긴다. 반대로 줄이면 이미 지난 걸음 수보다 작아져
585
+ * 그 자리에서 턴이 끝나 버릴 수도 있다 — 어느 쪽으로도 줄일 이유가 없다.
586
+ */
587
+ if (!session.stepsSet) maxSteps = Math.max(maxSteps, 걸음수(모드.id, conn.ctx));
588
+
589
+ return {
590
+ type: '단계옮김',
591
+ 옛: 옛모드,
592
+ 새: 갈곳,
593
+ // 화면 글이 아니라 **까닭 이름**을 낸다. 옮겨 적는 것은 화면의 몫이다
594
+ // (i18n/*.js 의 ev.phaseWhy*). 여기서 한국어를 내면 영어 화면에 섞인다.
595
+ 왜: 이번턴본것.바꿈 ? '바꿈' : '할일',
596
+ 늘어난도구: 늘어난것,
597
+ 걸음: maxSteps,
598
+ };
599
+ };
600
+
496
601
  const attempted = new Set(); // 같은 변경성 명령을 두 번 실행하지 않기 위한 기록
497
602
  let steps = 0;
498
603
  let lastToolFailed = false; // 직전 단계에서 도구가 오류를 냈나 → 다음 판단은 세게
@@ -688,6 +793,15 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
688
793
  while (steps < maxSteps) {
689
794
  steps++;
690
795
 
796
+ /*
797
+ * 단계를 **부르기 전에** 다시 본다 (위 단계옮기기).
798
+ *
799
+ * 부른 뒤에 옮기면 이번 걸음은 옛 시킴말로 나가고, 옮긴 보람이 한 걸음
800
+ * 늦는다. 마흔 걸음짜리 턴에서 그 한 걸음이 파일 내용 한 벌씩이다.
801
+ */
802
+ const 옮김 = 단계옮기기();
803
+ if (옮김) yield 옮김;
804
+
691
805
  // 부르기 **전에** 자리를 본다. 왜 여기인지는 자리만들기() 머리말에.
692
806
  yield* 자리만들기();
693
807
  if (접다멈췄나) return;
@@ -1817,6 +1931,18 @@ export async function* run(session, ctx, userText, { signal = null, 깊이 = 0,
1817
1931
  if (call.name === 'Read' && result.content) session.noteRead(call.args.file_path, result.content);
1818
1932
  // 실제로 파일이 바뀐 것만 적는다. 턴 끝에 이 목록을 디스크와 견준다.
1819
1933
  if (result.changed) 손댄파일.add(result.changed);
1934
+ /*
1935
+ * 단계도 이걸 본다 (agent/단계.js).
1936
+ *
1937
+ * 「바꾸기 시작했다」 는 조사가 끝났다는 뜻이다. 손댄파일 로 재도
1938
+ * 되지만 그 집합은 하위 작업이 바꾼 것까지 담으므로, 이 턴에서
1939
+ * **내가** 바꿨나를 따로 센다. 하위가 바꿨다고 부모의 단계가
1940
+ * 넘어가면, 부모는 아직 조사 중인데 시킴말만 바뀐다.
1941
+ */
1942
+ if (result.changed || result.여럿?.some((f) => f.ok) || result.바뀐것들?.length) {
1943
+ 이번턴본것.바꿈 = true;
1944
+ }
1945
+ if (call.name === 'TodoWrite' && !result.error) 이번턴본것.할일 = true;
1820
1946
  // 한 번에 여러 개를 쓴 경우. changed 하나만 보면 나머지가 조용히 빠져서,
1821
1947
  // 턴 끝에 "만들어졌다" 고 확인해 주는 파일이 넷 중 하나만 나온다.
1822
1948
  for (const f of result.여럿 ?? []) if (f.ok && f.path) 손댄파일.add(f.path);
@@ -18,7 +18,10 @@
18
18
  // Ask 는 **모든 모드**에 있다(읽기 갈래에 둔 이유가 그것이다). 갈림길은
19
19
  // 어느 모드에서나 생기고, 물어볼 길이 없으면 모델은 글로 "알려주세요" 하고
20
20
  // 턴을 끝내 버린다 — 그러면 여태 조사한 것이 통째로 버려진다.
21
- const 읽기 = ['Read', 'Outline', 'Glob', 'Grep', 'Def', 'Refs', 'WebFetch', 'Skill', 'Recall', 'Ask'];
21
+ // 내보낸다 loop.js 동시에 돌릴 목록이 이것보다 짧아지면 검사가 잡게(test/parallel.test.js).
22
+ // 두 곳에 같은 목록을 따로 적어 둔 탓에 다섯 개가 몇 달 동안 줄을 서서 돌았다.
23
+ export const 읽기도구 = ['Read', 'Outline', 'Glob', 'Grep', 'Def', 'Refs', 'WebFetch', 'Skill', 'Recall', 'Ask'];
24
+ const 읽기 = 읽기도구;
22
25
  // 계획을 적는 도구. 파일을 안 건드리므로 읽기 전용 모드에서도 준다.
23
26
  //
24
27
  // Remember 도 여기 있다. 기억은 사용자의 소스를 안 건드리고 .deel/memory.md
@@ -0,0 +1,133 @@
1
+ /**
2
+ * 종합 모드에서 **단계가 일을 따라간다.**
3
+ *
4
+ * ── 무엇이 문제였나 ─────────────────────────────────────────────────────
5
+ *
6
+ * 종합 모드는 한마디를 보고 알맞은 작업 모드를 고른다(agent/route.js). 그런데
7
+ * 그 고르기가 **턴이 시작할 때 딱 한 번** 돌았다. 고른 뒤로는 그 턴이 끝날
8
+ * 때까지 안 바뀐다.
9
+ *
10
+ * 짧은 한마디에는 맞는 방식인데, 긴 일에서는 그게 곧 고장이다. 한 턴이 마흔
11
+ * 걸음을 도는 동안 일의 성격은 몇 번씩 바뀐다 —
12
+ *
13
+ * 「왜 안 되는지 찾아서 고쳐줘」 → 디버그로 시작한다. 열두 걸음쯤에 원인을
14
+ * 찾는다. 그때부터 남은 스물여덟 걸음은 **고치는 일**인데, 시킴말은 여전히
15
+ * 「원인을 찾아라 · 고치기 전에 재현부터 하라」 고 말하고 있다.
16
+ *
17
+ * 화면에도 `~디버그` 가 끝까지 떠 있다. 사람이 「알아서 계획·디버그·코드로
18
+ * 넘어가 달라」 고 한 것이 이 자리다.
19
+ *
20
+ * ── 무엇으로 옮기나 ─────────────────────────────────────────────────────
21
+ *
22
+ * 모델에게 「지금 무슨 단계냐」 고 한 번 더 묻지 않는다. 그러면 걸음마다 왕복이
23
+ * 하나 늘고, 그 판단은 route.js 가 낱말로 하던 것보다 나을 것도 없다.
24
+ *
25
+ * **이미 일어난 일**로 옮긴다. 짐작이 아니라 잰 것이다.
26
+ *
27
+ * 할 일을 적었다 (TodoWrite 에 남은 일이 있다) → 계획은 끝났다
28
+ * 파일을 실제로 바꿨다 → 만드는 중이다
29
+ *
30
+ * ── 앞으로만 간다 ───────────────────────────────────────────────────────
31
+ *
32
+ * 되돌아가지 않는다. 한 걸음 읽었다고 다시 조사 단계로 내려가면 단계가
33
+ * 걸음마다 오르내리고, 그때마다 시킴말이 바뀐다 — 그건 캐시를 걸음마다
34
+ * 깨뜨리면서 모델에게는 갈피를 못 잡게 하는 짓이다.
35
+ *
36
+ * ── 사람이 고른 모드는 안 건드린다 ──────────────────────────────────────
37
+ *
38
+ * `/plan` 을 직접 친 사람에게 계획 모드는 **약속**이다. 여기서 옮기는 것은
39
+ * 라우터가 **짐작한** 단계뿐이다. 그 둘을 가르는 것은 `종합인가` 하나다.
40
+ */
41
+
42
+ /*
43
+ * ── 단계가 넘어가는 표 ──────────────────────────────────────────────────
44
+ *
45
+ * 왼쪽에서 오른쪽으로만 간다. 여기 없는 모드는 안 옮긴다.
46
+ *
47
+ * auto → code 무슨 일인지 못 고른 채로 시작했는데 파일을 바꾸기 시작했다.
48
+ * 그러면 지금 하는 일은 만드는 일이다.
49
+ * debug → code 원인을 찾아 고치기 시작했다. 남은 걸음은 만드는 일이다.
50
+ * plan → code 계획을 다 적었다. 다만 이건 조건이 하나 더 붙는다 (아래).
51
+ *
52
+ * `architect` · `inspect` · `ask` 는 **안 옮긴다.**
53
+ *
54
+ * 셋 다 라우터가 「이 턴은 말로 답하는 턴」 이라고 본 자리다. 그리고 그렇게
55
+ * 보려면 사람의 말에 고치라는 말이 없어야 한다(route.js 의 손대라했나 가
56
+ * 그 셋을 후보에서 뺀다). 즉 **사람이 고쳐 달라고 안 했다.** 안 시킨 일을
57
+ * 스스로 시작하는 것은 단계가 나아가는 것이 아니라 시킨 범위를 넘는 것이다.
58
+ */
59
+ const 넘어갈곳 = {
60
+ auto: 'code',
61
+ debug: 'code',
62
+ plan: 'code',
63
+ };
64
+
65
+ /*
66
+ * `plan` 만 조건이 더 붙는다. **두 개다.**
67
+ *
68
+ * 계획 모드는 파일을 바꾸는 도구가 없다. 그래서 여기서 넘어가면 도구가
69
+ * 늘어나고, 늘어난 도구는 앞머리를 바꾸므로 캐시가 한 번 끊긴다. 값이 드는
70
+ * 일이라 아무 때나 하면 안 된다. 그보다 더 큰 것은, 이 자리가 **시킨 범위를
71
+ * 넘을 수 있는 유일한 자리**라는 점이다 — 다른 단계는 이미 파일을 바꿀 수
72
+ * 있는 자리에서 시작하지만 계획은 아니다.
73
+ *
74
+ * (1) 사람이 「묻지 말고 끝까지」 라고 적어 뒀어야 한다.
75
+ *
76
+ * 「계획해서 만들어줘」 는 이미 다른 길이 있다 — 계획을 내고 승인을
77
+ * 받아 이어서 하는 길이다(route.js 의 겹친요청). 그 길이 있는데 여기서
78
+ * 몰래 넘어가면 사람이 보자고 한 계획을 못 보고 파일부터 바뀐다.
79
+ * 그런데 겹침 길은 「묻지 말라」 고 적은 사람에게는 꺼져 있다. 그 사람
80
+ * 에게는 승인 창이 값이 아니라 손해이기 때문이다. 그러면 계획만 내고
81
+ * 멈추는 턴이 남는다 — 시킨 일의 절반이다. 그 나머지 절반이 이 자리다.
82
+ *
83
+ * (2) 그리고 사람이 **고쳐 달라고 했어야 한다** (route.js 의 손대라했나).
84
+ *
85
+ * (1)만으로는 구멍이 하나 남는다. 「계획만 세워줘. 알아서 진행해」 처럼
86
+ * **계획만 원하면서 묻지 말라고도 적은** 말이 있다. 그 사람은 승인 창이
87
+ * 싫다고 했을 뿐 파일을 고쳐 달라고 한 적이 없다. 그런데 (1)만 보면
88
+ * 할 일을 적는 순간 코드로 넘어가 파일이 바뀐다.
89
+ *
90
+ * 두 말은 뜻이 다르다 — 「묻지 마라」 는 **어떻게** 하라는 말이고,
91
+ * 「고쳐라」 는 **무엇을** 하라는 말이다. 범위를 정하는 것은 뒤엣것이다.
92
+ * 그래서 둘 다 있어야 넘어간다.
93
+ */
94
+ const 말에있어야하는것 = {
95
+ plan: (말) => 말.묻지말라 === true && 말.손대라 === true,
96
+ };
97
+
98
+ /** 이 단계에서 다음으로 넘어가려면 무엇이 일어나야 하나. */
99
+ const 신호 = {
100
+ // 파일을 바꾸기 전에는 아직 조사 중이다.
101
+ auto: (본것) => 본것?.바꿈 === true,
102
+ debug: (본것) => 본것?.바꿈 === true,
103
+ // 계획은 파일을 못 바꾼다. 그러니 「바꿨나」 로는 영영 안 넘어간다.
104
+ // 계획이 끝났다는 신호는 **적어 둔 할 일**이다.
105
+ plan: (본것) => 본것?.할일 === true,
106
+ };
107
+
108
+ /**
109
+ * 지금 걸음에서 어느 단계여야 하나.
110
+ *
111
+ * @param {object} 것
112
+ * @param {string} 것.지금 지금 단계 (작업 모드 id)
113
+ * @param {boolean} 것.종합인가 사람이 고른 모드가 아니라 종합 모드인가
114
+ * @param {object} 것.본것 { 할일: boolean, 바꿈: boolean } — 이번 턴에 일어난 일
115
+ * @param {boolean} 것.묻지말라 사람이 「묻지 말고 끝까지」 라고 적어 뒀나
116
+ * @param {boolean} 것.손대라 사람이 고쳐 달라고 했나 (route.js 의 손대라했나)
117
+ * @returns {string|null} 옮길 곳. 안 옮기면 null.
118
+ */
119
+ export function 다음단계({ 지금, 종합인가, 본것, 묻지말라 = false, 손대라 = false }) {
120
+ // 사람이 고른 모드는 약속이다. 짐작만 옮긴다.
121
+ if (!종합인가) return null;
122
+ const 갈곳 = 넘어갈곳[지금];
123
+ if (!갈곳 || 갈곳 === 지금) return null;
124
+ const 잠금 = 말에있어야하는것[지금];
125
+ if (잠금 && !잠금({ 묻지말라, 손대라 })) return null;
126
+ return 신호[지금]?.(본것) ? 갈곳 : null;
127
+ }
128
+
129
+ /** 넘어가면서 도구가 늘어나나 — 늘면 앞머리를 다시 쓰게 되므로 사람에게 말해야 한다. */
130
+ export function 도구가늘어나나(옛모드, 새모드, 도구가져오기) {
131
+ const 옛 = new Set(도구가져오기(옛모드));
132
+ return 도구가져오기(새모드).some((t) => !옛.has(t));
133
+ }
package/src/i18n/en.js CHANGED
@@ -474,6 +474,10 @@ export const en = {
474
474
  'run.stopping': 'stopping…',
475
475
  'run.errKept': 'What arrived before the failure was kept in the conversation — you can carry on from there.',
476
476
  'ev.capped': 'The answer was cut off at {한계} tokens — raise the cap with /out, or have files written in parts',
477
+ 'ev.phaseMove': '{왜} — from here on this runs as {새}, not {옛}',
478
+ 'ev.phaseWhyEdit': 'Started changing files',
479
+ 'ev.phaseWhyTodo': 'The plan is written down',
480
+ 'ev.phaseTools': 'More tools from here ({도구}) — the prefix gets written once more',
477
481
  'ev.cutoff': 'The server stopped the answer without saying it had finished — it may have been cut mid-way',
478
482
  'ev.stalled': 'Nothing arrived for {n}s, so the connection was dropped — this is what came through',
479
483
  'ev.nudgeMissed': 'It was about to finish leaving {n} of the things you asked for undone — pushed back once',
package/src/i18n/ja.js CHANGED
@@ -432,6 +432,10 @@ export const ja = {
432
432
  'run.stopping': '止めています…',
433
433
  'run.errKept': 'ここまで届いた分は会話に残しました — このまま続けて話せます。',
434
434
  'ev.capped': '答えが {한계} トークンで切れました — /out で上限を上げるか、ファイルは分けて書かせてください',
435
+ 'ev.phaseMove': '{왜} — ここからは {옛} ではなく {새} で進めます',
436
+ 'ev.phaseWhyEdit': 'ファイルを変更し始めました',
437
+ 'ev.phaseWhyTodo': 'やることを書き終えました',
438
+ 'ev.phaseTools': 'ツールが増えました ({도구}) — 先頭を一度書き直します',
435
439
  'ev.cutoff': 'サーバーが終わったとも言わずに答えを止めました — 途中で切れた可能性があります',
436
440
  'ev.stalled': '{n}秒間なにも届かなかったので切りました — 届いたぶんだけ表示します',
437
441
  'ev.nudgeMissed': '頼んだこと {n} 件を残したまま終えようとしたので、一度押し戻しました',
package/src/i18n/ko.js CHANGED
@@ -505,6 +505,10 @@ export const ko = {
505
505
  * `deel -p` 뒤 스크립트로 넘어가던 것이 그 탓이다.
506
506
  */
507
507
  'ev.capped': '답이 {한계} 토큰에서 잘렸습니다 — /out 으로 상한을 올리거나, 파일은 나눠 쓰게 하세요',
508
+ 'ev.phaseMove': '{왜} — 여기서부터 {옛} 대신 {새} 로 갑니다',
509
+ 'ev.phaseWhyEdit': '파일을 바꾸기 시작했습니다',
510
+ 'ev.phaseWhyTodo': '할 일을 다 적었습니다',
511
+ 'ev.phaseTools': '도구가 늘었습니다 ({도구}) — 앞머리를 한 번 다시 씁니다',
508
512
  'ev.cutoff': '서버가 끝났다는 말 없이 답을 멈췄습니다 — 중간에서 끊겼을 수 있습니다',
509
513
  'ev.stalled': '{n}초 동안 아무것도 안 와서 끊었습니다 — 받은 데까지만 보여 드립니다',
510
514
  'ev.nudgeMissed': '시킨 것 {n}가지를 빼놓고 끝내려고 해서 한 번 되밀었습니다',
package/src/i18n/zh.js CHANGED
@@ -432,6 +432,10 @@ export const zh = {
432
432
  'run.stopping': '正在停止…',
433
433
  'run.errKept': '出错前收到的内容已经留在对话里 — 可以直接接着说。',
434
434
  'ev.capped': '回答在 {한계} 个 token 处被截断 — 用 /out 调高上限,或者让文件分次写',
435
+ 'ev.phaseMove': '{왜} — 从这里开始按 {새} 而不是 {옛} 进行',
436
+ 'ev.phaseWhyEdit': '开始修改文件了',
437
+ 'ev.phaseWhyTodo': '要做的事已经列完了',
438
+ 'ev.phaseTools': '工具变多了 ({도구}) — 前缀要重写一次',
435
439
  'ev.cutoff': '服务端没说结束就停下了 — 回答可能是中途断的',
436
440
  'ev.stalled': '{n} 秒没有任何数据,已断开 — 只显示已收到的部分',
437
441
  'ev.nudgeMissed': '还剩你交代的 {n} 件事没做就要收尾,已经推回去一次',
package/src/oneshot.js CHANGED
@@ -38,7 +38,7 @@ import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
38
38
  import { 잠잠기본 } from './backend/http.js';
39
39
  import { 인증서설정 } from './backend/clientcert.js';
40
40
  import { route } from './agent/route.js';
41
- import { get as getWork } from './agent/modes.js';
41
+ import { get as getWork, 보일이름 } from './agent/modes.js';
42
42
  import { 모두끝내기 as 일감모두끝내기, 일감인자 } from './tools/jobs.js';
43
43
  import { 첫이름 } from './tools/label.js';
44
44
 
@@ -612,6 +612,21 @@ export async function runOnce(opts = {}) {
612
612
  곁(` ${c.cyan('⚙')} ${c.gray(옮긴말('loop.wire', { 무엇: ev.무엇 }))}`);
613
613
  break;
614
614
 
615
+ /*
616
+ * 종합 모드가 턴 도중에 단계를 옮겼다 (agent/단계.js).
617
+ *
618
+ * 한 방 실행에서 특히 남겨야 하는 줄이다. `deel -p` 는 잡·CI 에서 돌고
619
+ * 그 기록만 나중에 남는다 — 「계획만 내고 끝날 줄 알았는데 파일이
620
+ * 바뀌어 있다」 를 되짚을 자리가 여기 말고는 없다.
621
+ */
622
+ case '단계옮김':
623
+ 곁(` ${c.hcyan('→')} ${c.gray(옮긴말('ev.phaseMove', {
624
+ 왜: 옮긴말(ev.왜 === '바꿈' ? 'ev.phaseWhyEdit' : 'ev.phaseWhyTodo'),
625
+ 옛: 보일이름(ev.옛),
626
+ 새: 보일이름(ev.새),
627
+ }))}`);
628
+ break;
629
+
615
630
  case 'folded':
616
631
  곁(` ${c.cyan('◲')} ${c.gray(`오래된 도구 결과 ${ev.접은것}개를 접었습니다 (${ev.아낀토큰.toLocaleString()} 토큰을 비움)`)}`);
617
632
  break;
package/src/repl.js CHANGED
@@ -2137,6 +2137,31 @@ export async function chatLoop(opts = {}) {
2137
2137
  }
2138
2138
  break;
2139
2139
 
2140
+ /*
2141
+ * 종합 모드가 **단계를 옮겼다** (agent/단계.js).
2142
+ *
2143
+ * 이 줄이 없으면 사람은 화면의 모드 표시가 턴 도중에 왜 바뀌었는지
2144
+ * 모른다 — 자기가 누른 적도 없는데 `~디버그` 가 `~코드` 로 바뀌어
2145
+ * 있으면 그건 알림이 아니라 버그처럼 보인다. 그래서 무엇을 보고
2146
+ * 옮겼는지(`왜`) 를 같이 적는다.
2147
+ *
2148
+ * 도구가 늘어난 경우는 한 줄 더 붙인다. 규격이 도구 목록을 앞머리
2149
+ * 맨 앞에 렌더하므로, 도구가 하나 늘면 그 턴은 앞머리를 캐시에서 못
2150
+ * 읽고 다시 쓴다. 접기(folded)에서 그 대가를 적기 시작한 것과 같은
2151
+ * 이유다 — 이득만 보이고 값은 며칠 뒤 청구서에서 보는 일이 없게.
2152
+ */
2153
+ case '단계옮김': {
2154
+ const 왜 = 옮긴말(ev.왜 === '바꿈' ? 'ev.phaseWhyEdit' : 'ev.phaseWhyTodo');
2155
+ say(` ${c.hcyan('→')} ${c.gray(옮긴말('ev.phaseMove', { 왜, 옛: 보일이름(ev.옛), 새: 보일이름(ev.새) }))}`);
2156
+ if ((ev.늘어난도구?.length ?? 0) > 0 && session.level !== '쉬움') {
2157
+ const 보일수 = Math.min(5, ev.늘어난도구.length);
2158
+ const 이름들 = ev.늘어난도구.slice(0, 보일수).join(' · ')
2159
+ + (ev.늘어난도구.length > 보일수 ? ` +${ev.늘어난도구.length - 보일수}` : '');
2160
+ say(` ${c.gray(옮긴말('ev.phaseTools', { 도구: 이름들 }))}`);
2161
+ }
2162
+ break;
2163
+ }
2164
+
2140
2165
  case 'trimmed':
2141
2166
  say(` ${c.gray(`(컨텍스트가 차서 오래된 대화 ${ev.dropped}개를 줄였습니다)`)}`);
2142
2167
  break;