deel-local-cli 1.6.1 → 1.6.2

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.md CHANGED
@@ -989,6 +989,7 @@ zip 은 진짜 `unzip` 으로, tar 는 진짜 `tar` 가 만든 것을 읽혀 교
989
989
 
990
990
  | 판 | 무엇이 바뀌었나 |
991
991
  |---|---|
992
+ | **[1.6.2](docs/ko/releases.md#162)** | 누르면 멈추고, 시킨 대로 하고, 안 끊긴다 — ESC · 되묻기 · 요청 누락 · 붙여넣기 접기 · 끊긴 자리 잇기 |
992
993
  | **[1.6.1](docs/ko/releases.md#161)** | 헛도는 턴을 만들던 자리들 — 울타리가 `/dev/null` 까지 막던 것 · 옷 문서를 못 읽던 것 · 파일에서 그림이 사라지던 것 |
993
994
  | **[1.6.0](docs/ko/releases.md#160)** | 사내망에서 안 되던 것들 — 프록시 · 429 · 윈도우 셸 · 5만 개 저장소 · PDF · 캡처 붙여넣기 · 에디터 이어하기 |
994
995
  | **[1.5.8](docs/ko/releases.md#158)** | 5MB 문서를 919줄 중 8줄만 보던 것 · 정규식을 경로로 읽어 명령을 막던 것 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deel-local-cli",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "로컬 모델·사내 게이트웨이 전용 코딩 에이전트 CLI — 외부 의존성 0개 / Zero-dependency coding agent CLI for local LLMs and private gateways",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -51,7 +51,7 @@
51
51
  "verify": "node test/no-bundle.test.js && node test/network.test.js && node test/web.test.js",
52
52
  "bench": "node test/edit-bench.js",
53
53
  "demo": "node test/demo.js",
54
- "check": "node --check bin/deel.js && node --check src/repl.js && node --check src/oneshot.js && node --check src/commands.js && node --check src/agent/loop.js && node --check src/agent/session.js && node --check src/agent/effort.js && node --check src/agent/modes.js && node --check src/agent/route.js && node --check src/agent/models.js && node --check src/agent/grade.js && node --check src/agent/mention.js && node --check src/agent/project.js && node --check src/tools/encoding.js && node --check src/tools/excel-com.js && node --check src/tools/excel.js && node --check src/tools/xlsx.js && node --check src/tools/docs.js && node --check src/tools/convert.js && node --check src/tools/pdf.js && node --check src/tools/clipboard.js && node --check src/agent/compact.js && node --check src/agent/store.js && node --check src/agent/sessionui.js && node --check src/backend/adapter.js && node --check src/backend/ctxsize.js && node --check src/backend/probe.js && node --check src/backend/detect.js && node --check src/backend/http.js && node --check src/backend/retry.js && node --check src/backend/proxy.js && node --check src/backend/azure.js && node --check src/backend/vision.js && node --check src/backend/quota.js && node --check src/backend/scan.js && node --check src/backend/scanui.js && node --check src/tools/index.js && node --check src/tools/desc.en.js && node --check src/tools/lsp.js && node --check src/lsp/rpc.js && node --check src/lsp/servers.js && node --check src/lsp/client.js && node --check src/lsp/diag.js && node --check src/tools/fsutil.js && node --check src/tools/ignore.js && node --check src/tools/fastgrep.js && node --check src/tools/edit-match.js && node --check src/tools/webfetch.js && node --check src/tools/todo.js && node --check src/tools/jobs.js && node --check src/tools/shell.js && node --check src/skills/discover.js && node --check src/plugins/manage.js && node --check src/pack/zip.js && node --check src/pack/tar.js && node --check src/pack/selfpack.js && node --check src/safety/guard.js && node --check src/safety/undo.js && node --check src/safety/audit.js && node --check src/safety/keystore.js && node --check src/safety/policy.js && node --check src/safety/network.js && node --check src/setup.js && node --check src/report.js && node --check src/config.js && node --check src/completion.js && node --check src/ui/diff.js && node --check src/ui/ansi.js && node --check src/ui/status.js && node --check src/ui/level.js && node --check src/ui/prompt.js && node --check src/ui/spinner.js && node --check src/ui/motion.js && node --check src/ui/office.js && node --check src/ui/notify.js && node --check src/ui/intro.js && node --check src/ui/banner.js && node --check src/ui/export.js && node --check src/i18n/index.js && node --check src/i18n/ko.js && node --check src/i18n/en.js && node --check src/i18n/ja.js && node --check src/i18n/zh.js && node --check src/preview/serve.js && node --check src/ui/working.js && node --check src/ui/inputbox.js && node --check src/ui/wrap.js && node --check src/ui/md.js && node --check src/agent/threads.js && node --check src/agent/evolve.js && node --check src/agent/pins.js && node --check src/agent/card.js && node --check src/agent/preset.js && node --check src/agent/evidence.js && node --check src/agent/commit.js && node --check src/agent/review.js && node --check src/acp/jsonrpc.js && node --check src/acp/map.js && node --check src/acp/serve.js && node --check src/pack/sbom.js && node --check src/safety/secrets.js && echo OK",
54
+ "check": "node --check bin/deel.js && node --check src/repl.js && node --check src/oneshot.js && node --check src/commands.js && node --check src/agent/loop.js && node --check src/agent/session.js && node --check src/agent/effort.js && node --check src/agent/modes.js && node --check src/agent/route.js && node --check src/agent/askcheck.js && node --check src/agent/asks.js && node --check src/agent/models.js && node --check src/agent/grade.js && node --check src/agent/mention.js && node --check src/agent/project.js && node --check src/tools/encoding.js && node --check src/tools/excel-com.js && node --check src/tools/excel.js && node --check src/tools/xlsx.js && node --check src/tools/docs.js && node --check src/tools/convert.js && node --check src/tools/pdf.js && node --check src/tools/clipboard.js && node --check src/agent/compact.js && node --check src/agent/store.js && node --check src/agent/sessionui.js && node --check src/backend/adapter.js && node --check src/backend/ctxsize.js && node --check src/backend/probe.js && node --check src/backend/detect.js && node --check src/backend/http.js && node --check src/backend/retry.js && node --check src/backend/proxy.js && node --check src/backend/azure.js && node --check src/backend/vision.js && node --check src/backend/quota.js && node --check src/backend/scan.js && node --check src/backend/scanui.js && node --check src/tools/index.js && node --check src/tools/desc.en.js && node --check src/tools/lsp.js && node --check src/lsp/rpc.js && node --check src/lsp/servers.js && node --check src/lsp/client.js && node --check src/lsp/diag.js && node --check src/tools/fsutil.js && node --check src/tools/ignore.js && node --check src/tools/spawn.js && node --check src/tools/fastgrep.js && node --check src/tools/edit-match.js && node --check src/tools/webfetch.js && node --check src/tools/todo.js && node --check src/tools/jobs.js && node --check src/tools/shell.js && node --check src/skills/discover.js && node --check src/plugins/manage.js && node --check src/pack/zip.js && node --check src/pack/tar.js && node --check src/pack/selfpack.js && node --check src/safety/guard.js && node --check src/safety/undo.js && node --check src/safety/audit.js && node --check src/safety/keystore.js && node --check src/safety/policy.js && node --check src/safety/network.js && node --check src/setup.js && node --check src/report.js && node --check src/config.js && node --check src/completion.js && node --check src/ui/diff.js && node --check src/ui/ansi.js && node --check src/ui/status.js && node --check src/ui/level.js && node --check src/ui/prompt.js && node --check src/ui/spinner.js && node --check src/ui/motion.js && node --check src/ui/office.js && node --check src/ui/notify.js && node --check src/ui/intro.js && node --check src/ui/banner.js && node --check src/ui/export.js && node --check src/i18n/index.js && node --check src/i18n/ko.js && node --check src/i18n/en.js && node --check src/i18n/ja.js && node --check src/i18n/zh.js && node --check src/preview/serve.js && node --check src/ui/working.js && node --check src/ui/inputbox.js && node --check src/ui/wrap.js && node --check src/ui/pastechip.js && node --check src/ui/md.js && node --check src/agent/threads.js && node --check src/agent/evolve.js && node --check src/agent/pins.js && node --check src/agent/card.js && node --check src/agent/preset.js && node --check src/agent/evidence.js && node --check src/agent/commit.js && node --check src/agent/review.js && node --check src/acp/jsonrpc.js && node --check src/acp/map.js && node --check src/acp/serve.js && node --check src/pack/sbom.js && node --check src/safety/secrets.js && echo OK",
55
55
  "coverage": "node test/coverage.mjs",
56
56
  "prepublishOnly": "npm run check && npm test"
57
57
  },
@@ -0,0 +1,281 @@
1
+ // 되묻기 전에 통과해야 하는 관문.
2
+ //
3
+ // ── 무엇을 막나 ────────────────────────────────────────────────────────
4
+ //
5
+ // 실제로 받은 제보다. 사용자가 네 가지를 번호까지 매겨 적어 줬는데, deel 이
6
+ // 한참 뒤에 이렇게 물었다 —
7
+ //
8
+ // ┌ 물어봅니다
9
+ // │ 어떤 것부터 진행할까요?
10
+ // │ 1 붙여넣기 표시
11
+ // │ 2 ESC 중단
12
+ // │ 3 요청 누락
13
+ // │ 4 여러 파일 편집
14
+ // └ 번호로 골라 주세요
15
+ //
16
+ // 사용자가 적어 준 목록을 그대로 되읽어 준 것이다. 새로 알아낸 것도, 정해야
17
+ // 할 갈림길도 없다. 사람 입장에서는 **읽지도 않고 되묻는** 것으로 보인다.
18
+ // "이렇게 내용 못 알아먹고 무작정 계획표 질문하는 이런 것 좀 없으면 좋겠어."
19
+ //
20
+ // ── 왜 프롬프트로 안 하나 ───────────────────────────────────────────────
21
+ //
22
+ // 도구 설명에 이미 적혀 있었다 — "이미 사용자가 말한 것을 다시 묻지 마라."
23
+ // 그래도 이런 일이 났다. 작은 모델은 그 말을 잘 안 듣고, 큰 모델도 컨텍스트가
24
+ // 길어지면 앞의 규칙보다 지금 눈앞의 목록에 이끌린다.
25
+ //
26
+ // 그래서 여기서 막는다. 막는 것은 모델의 협조가 필요 없다.
27
+ //
28
+ // ── 무엇을 못 하나 ──────────────────────────────────────────────────────
29
+ //
30
+ // 이 관문은 **뜻을 모른다.** 글자만 본다. 그래서 "정말 물을 만한 것인가" 는
31
+ // 판단하지 않는다 — 그건 애초에 글자로 알 수 없다. 대신 글자로 확실히 알 수
32
+ // 있는 것만 막는다: 이해를 안 적었나 · 사용자가 이미 적어 준 것을 되읽나 ·
33
+ // 한 번에 여럿을 묻나. 애매하면 통과시킨다. 못 묻게 막는 것이 더 나쁘다.
34
+
35
+ import { 손대라했나 } from './route.js';
36
+ /*
37
+ * 두 글자씩 쪼갠다.
38
+ *
39
+ * 낱말로 자르면 한국어에서 안 맞는다 — '붙여넣기' 와 '붙여넣기한' 과
40
+ * '붙여넣기를' 이 서로 다른 낱말이 된다. 조사가 붙는 말에서 낱말 짝짓기는
41
+ * 거의 언제나 빗나간다.
42
+ *
43
+ * 두 글자 조각으로 보면 그 셋이 '붙여·여넣·넣기' 를 함께 가진다. 영어에도
44
+ * 그대로 통한다(postgres → po·os·st…). 빈칸과 문장부호는 지운다 — 사람이
45
+ * 띄어쓰기를 어떻게 하든 같은 것으로 봐야 한다.
46
+ */
47
+ export function 두글자들(글) {
48
+ const 민글 = String(글 ?? '').toLowerCase().replace(/[^0-9a-z가-힣ㄱ-ㅎㅏ-ㅣ]/g, '');
49
+ const 것 = new Set();
50
+ for (let i = 0; i + 2 <= 민글.length; i++) 것.add(민글.slice(i, i + 2));
51
+ return 것;
52
+ }
53
+
54
+ /** 앞엣것이 뒤엣것 안에 얼마나 들어 있나. 0~1. 잴 것이 없으면 0. */
55
+ export function 얼마나들었나(작은것, 큰것) {
56
+ const a = 두글자들(작은것);
57
+ if (!a.size) return 0;
58
+ const b = 두글자들(큰것);
59
+ let 든것 = 0;
60
+ for (const x of a) if (b.has(x)) 든것 += 1;
61
+ return 든것 / a.size;
62
+ }
63
+
64
+ /*
65
+ * 사용자가 낱낱이 적어 준 항목이 몇 개인가.
66
+ *
67
+ * 번호(1. 2)·글머리(- ·)·동그라미 숫자를 센다. 줄머리에 있는 것만 센다 —
68
+ * 글 가운데 있는 "1.6.1" 같은 것을 항목으로 세면 안 된다.
69
+ */
70
+ export function 항목수(요청) {
71
+ const 줄들 = String(요청 ?? '').split('\n');
72
+ let 몇 = 0;
73
+ for (const 줄 of 줄들) {
74
+ if (/^\s*(\d{1,2}\s*[.)]|[-*·▪️•]|[①-⑮])\s+\S/.test(줄)) 몇 += 1;
75
+ }
76
+ return 몇;
77
+ }
78
+
79
+ /*
80
+ * 알맹이가 없는 이해.
81
+ *
82
+ * "요청을 이해했습니다" 는 이해가 아니다. 무엇으로 이해했는지가 없으면
83
+ * 사람은 그 말을 읽고도 deel 이 제대로 봤는지 알 수 없다 — 그럴 바에는
84
+ * 안 적은 것과 같다.
85
+ */
86
+ const 빈말 = [
87
+ /^(사용자|당신)?\s*(의)?\s*(요청|말씀|내용|질문)\s*(을|를)?\s*(잘)?\s*(이해|확인|파악)\s*(했|하였|하겠|합니)/,
88
+ /^(무엇|뭘|어떤\s*것)\s*(을|를)?\s*(원하|바라|하고\s*싶)/,
89
+ /^(어떻게|어떤\s*식으로)\s*(진행|처리|작업)\s*(할지|하면)/,
90
+ /^(더|좀)?\s*(자세한|정확한|구체적인)\s*(내용|정보|설명)\s*(이)?\s*(필요|있어야)/,
91
+ /^i\s+understand\s+(your\s+)?(request|question)/i,
92
+ /^(to\s+)?(better\s+)?(understand|clarify)\s+(what|your)/i,
93
+ ];
94
+
95
+ /** 알맹이 없는 이해인가. */
96
+ export function 빈이해인가(이해) {
97
+ const s = String(이해 ?? '').trim();
98
+ return 빈말.some((re) => re.test(s));
99
+ }
100
+
101
+ // '무엇부터 할까요' 꼴. 이 물음이 위험한 것은, 사용자가 이미 적어 준 목록
102
+ // 앞에서 나올 때다. 그때는 정할 것이 없다 — 순서를 정하는 것이 deel 의 일이다.
103
+ const 고르라는물음 = /어(느|떤)\s*(것|거|쪽|부분|순서)|무엇(부터|을)|뭐(부터|를)|어디(부터|서부터)|먼저\s*(할|진행|하)|우선\s*순위|순서(를|는)?\s*(어떻|정)|which\s+(one|of these)|where\s+(should|would)\s+(i|we)\s+start/i;
104
+
105
+ /*
106
+ * 「그래서 무슨 일을 할까요」 꼴.
107
+ *
108
+ * 위의 고르라는물음 보다 **훨씬 좁다.** 이건 갈림길을 묻는 말이 아니라
109
+ * 무엇을 할지 자체를 되묻는 말이다. 사람이 이미 "만들어 줘" 라고 했는데
110
+ * 이 말이 나오면, 읽기는 했는데 못 알아들은 것이다.
111
+ *
112
+ * 실제로 받은 화면이 이랬다. 사람은 「부문별로 A4 가로 2쪽 PDF 를 OCR 내용
113
+ * 기반으로 따로따로 만들어 줘」 라고 했는데 —
114
+ *
115
+ * ┌ 물어봅니다
116
+ * │ 어떤 작업을 진행할까요?
117
+ * │ 1 내용·디자인 검토 2 내용 업데이트 3 PDF 생성·검증 4 문구 수정
118
+ * └ 번호로 골라 주세요
119
+ *
120
+ * 답을 주고 나서도 몇 걸음 뒤에 거의 같은 물음이 또 나왔다.
121
+ *
122
+ * '어느 쪽으로 할까요' 는 여기 안 넣는다. 그건 진짜 갈림길일 때가 많다.
123
+ */
124
+ const 무슨일할까 = /(어떤|무슨)\s*(작업|일)(을|를)?\s*(진행|할|하)|무엇을\s*(할|진행|해\s*드릴)|뭘\s*(할|해\s*드릴)|어떤\s*것부터|무엇부터|뭐부터|what\s+(should|would)\s+(i|we)\s+(do|work\s+on)|how\s+(should|would)\s+(i|we)\s+proceed/i;
125
+
126
+ /*
127
+ * 이만큼 겹치면 "이미 물어본 것" 으로 본다.
128
+ *
129
+ * 같은 것 문턱(0.5)보다 조금 낮게 둔다. 두 번째 물음은 첫 번째를 그대로
130
+ * 베끼지 않고 조금 줄여서 다시 적기 때문이다 — 위 화면에서도
131
+ * "현재 A3 경영진보고 편집 파일과 원본 자료를 확인했습니다. 어떤 작업을
132
+ * 진행할까요?" 가 "A3 경영진 보고 편집본에서 어떤 작업을 진행할까요?" 로
133
+ * 바뀌어 다시 나왔다.
134
+ */
135
+ const 또물음문턱 = 0.45;
136
+
137
+ // 한 턴에 이만큼까지만. 세 번 물으면 그건 대화가 아니라 뺑뺑이다.
138
+ const 한턴에최대 = 2;
139
+
140
+ /*
141
+ * 이만큼 겹치면 "사용자가 이미 적어 준 것" 으로 본다.
142
+ *
143
+ * 값을 재서 정했다. 처음에 0.6 으로 뒀더니 제보에 나온 그 물음을 못 잡았다 —
144
+ * 모델은 사용자 말을 그대로 베끼지 않고 **줄여서 다시 적기** 때문이다.
145
+ *
146
+ * "붙여넣기 해도 창에 붙여넣기된 내용이 다 뜨는 게 아니야"
147
+ * → 선택지 "붙여넣기 내용 표시" 0.57
148
+ * "다른 cli 쓰다가 deel 쓰면 기능적으로…"
149
+ * → 선택지 "다른 cli 대비 기능" 0.63
150
+ *
151
+ * 반대쪽도 쟀다. 진짜 갈림길의 선택지는 0.2 언저리다 —
152
+ * "버린다 — 긴 붙여넣기에서 메모리를 아낀다" 0.19
153
+ * '붙여넣기' 라는 말이 그대로 들어 있는데도 그렇다. 나머지가 다 새 말이라서다.
154
+ *
155
+ * 0.5 는 그 사이다. 여기서 더 내리면 진짜 갈림길이 막히기 시작한다.
156
+ */
157
+ const 같은것문턱 = 0.5;
158
+
159
+ /**
160
+ * 이 물음을 사람에게 내보내도 되나.
161
+ *
162
+ * @param {object} 것
163
+ * @param {string} 것.물음
164
+ * @param {string[]} [것.고를것]
165
+ * @param {string} [것.이해] 이번 요청을 무엇으로 알아들었는지
166
+ * @param {string} [것.요청] 사용자가 실제로 친 말
167
+ * @returns {{ok: true} | {ok: false, 왜: string, 할말: string}}
168
+ * 할말 은 모델에게 그대로 돌려줄 글이다 — 무엇이 모자란지와
169
+ * 대신 무엇을 하라는 것까지 적는다. "안 됩니다" 만 돌려주면
170
+ * 모델은 같은 것을 다시 부른다.
171
+ */
172
+ export function 물음검사({ 물음 = '', 고를것 = [], 이해 = '', 요청 = '', 이미물은것 = [] } = {}) {
173
+ const 물 = String(물음 ?? '').trim();
174
+ const 이 = String(이해 ?? '').trim();
175
+ const 것들 = (Array.isArray(고를것) ? 고를것 : []).map((x) => String(x ?? '').trim()).filter(Boolean);
176
+
177
+ // ── 1. 이해를 먼저 말한다 ────────────────────────────────────────────
178
+ if (이.length < 10) {
179
+ return {
180
+ ok: false,
181
+ 왜: '이해가 없음',
182
+ 할말: '묻기 전에 `이해` 를 채우세요 — 이번 요청을 무엇으로 알아들었는지 한 줄로.'
183
+ + ' 사람은 그 줄을 보고 네가 제대로 읽었는지 판단합니다.'
184
+ + ' 그 줄을 못 쓰겠으면 아직 물을 때가 아닙니다. 먼저 읽어 보세요.',
185
+ };
186
+ }
187
+ if (빈이해인가(이)) {
188
+ return {
189
+ ok: false,
190
+ 왜: '이해가 알맹이 없음',
191
+ 할말: `"${이}" 는 이해가 아니라 인사말입니다. **무엇을** 하라는 것으로 알아들었는지`
192
+ + ' 그 일의 내용을 넣어 다시 적으세요 (예: "붙여넣은 글이 화면에 다 안 뜨는 것을'
193
+ + ' 접힌 표시로 바꾸라는 것으로 이해했습니다").',
194
+ };
195
+ }
196
+
197
+ // ── 2. 이미 적어 준 것을 되읽지 않는다 ───────────────────────────────
198
+ //
199
+ // 선택지 하나하나가 사용자 말 안에 이미 들어 있으면, 그건 갈림길이 아니라
200
+ // 목록을 되읽어 준 것이다. 둘 이상이면 그렇게 본다 — 하나는 우연일 수 있다.
201
+ const 이미있는것 = 것들.filter((x) => 얼마나들었나(x, 요청) >= 같은것문턱);
202
+ if (이미있는것.length >= 2) {
203
+ return {
204
+ ok: false,
205
+ 왜: `선택지 ${이미있는것.length}개가 이미 사용자 말 안에 있음`,
206
+ 할말: `선택지로 내민 것 중 ${이미있는것.length}개(${이미있는것.slice(0, 3).join(' · ')})는`
207
+ + ' 사용자가 이미 적어 준 것입니다. 그건 갈림길이 아니라 할 일 목록입니다.'
208
+ + ' 묻지 말고 **전부** 하세요. 순서를 정하는 것은 네 일입니다 —'
209
+ + ' TodoWrite 로 적고 시작하세요.',
210
+ };
211
+ }
212
+
213
+ // 선택지 없이 "무엇부터 할까요" 만 던지는 것도 같다.
214
+ const 적어준것 = 항목수(요청);
215
+ if (적어준것 >= 2 && 고르라는물음.test(물)) {
216
+ return {
217
+ ok: false,
218
+ 왜: `사용자가 ${적어준것}개를 적어 줬는데 무엇부터냐고 물음`,
219
+ 할말: `사용자가 이미 ${적어준것}가지를 적어 줬습니다. 무엇부터 할지는 묻는 것이 아니라`
220
+ + ' 네가 정하는 것입니다. TodoWrite 로 그 목록을 그대로 적고 첫 번째부터 시작하세요.'
221
+ + ' 순서를 바꾸는 편이 낫다고 보면 그 까닭을 한 줄로 밝히고 그렇게 하세요.',
222
+ };
223
+ }
224
+
225
+ // ── 3. 하라고 한 일을 두고 "무슨 일 할까요" 라고 되묻지 않는다 ───────
226
+ if (손대라했나(요청) && 무슨일할까.test(물)) {
227
+ return {
228
+ ok: false,
229
+ 왜: '하라고 한 일을 두고 무슨 일을 할지 되물음',
230
+ 할말: '사용자는 이미 무엇을 하라고 말했습니다. 무슨 일을 할지는 묻는 것이 아닙니다.'
231
+ + ' 그 말대로 하세요 — 시킨 것을 TodoWrite 로 적고 첫 번째부터 시작하세요.'
232
+ + ' 정말 정할 것이 남았으면 그 **한 가지**만 콕 집어 물으세요'
233
+ + ' (예: "글꼴 파일이 zip 안에 둘인데 어느 것을 쓸까요?").',
234
+ };
235
+ }
236
+
237
+ // ── 4. 같은 것을 또 묻지 않는다 ──────────────────────────────────────
238
+ //
239
+ // 물어보고, 답을 듣고, 몇 걸음 뒤에 거의 같은 것을 또 묻는 화면을 받았다.
240
+ // 사람 눈에는 앞의 대답이 통째로 무시된 것으로 보인다 — 실제로도 그렇다.
241
+ const 앞것 = (Array.isArray(이미물은것) ? 이미물은것 : []);
242
+ for (const 앞 of 앞것) {
243
+ const 앞물음 = String(앞?.물음 ?? 앞 ?? '');
244
+ if (!앞물음) continue;
245
+ if (얼마나들었나(물, 앞물음) < 또물음문턱) continue;
246
+ const 답 = String(앞?.답 ?? '').trim();
247
+ return {
248
+ ok: false,
249
+ 왜: '이번 턴에 이미 물어본 것',
250
+ 할말: `이미 물었습니다 — "${앞물음}"`
251
+ + (답 ? `\n사람의 답은 이것이었습니다: "${답}"` : '\n사람이 답을 안 했습니다.')
252
+ + '\n같은 것을 또 묻지 마세요. 그 답대로 진행하세요.'
253
+ + ' 답이 애매했으면 애매한 채로 가장 그럴듯한 쪽을 골라 진행하고,'
254
+ + ' 무엇을 그렇게 정했는지 끝에 한 줄 적으세요.',
255
+ };
256
+ }
257
+
258
+ if (앞것.length >= 한턴에최대) {
259
+ return {
260
+ ok: false,
261
+ 왜: `한 턴에 ${앞것.length}번 물음`,
262
+ 할말: `이번 턴에 이미 ${앞것.length}번 물었습니다. 더 묻지 말고 진행하세요.`
263
+ + ' 세 번을 묻는다는 것은 물음으로 풀 문제가 아니라는 뜻입니다 —'
264
+ + ' 지금 아는 것으로 가장 그럴듯한 쪽을 골라서 하고, 무엇을 가정했는지 끝에 적으세요.',
265
+ };
266
+ }
267
+
268
+ // ── 5. 한 번에 하나만 ────────────────────────────────────────────────
269
+ const 물음표 = (물.match(/[??]/g) ?? []).length;
270
+ if (물음표 >= 2) {
271
+ return {
272
+ ok: false,
273
+ 왜: '한 번에 여럿을 물음',
274
+ 할말: '한 번에 하나만 물으세요. 두 가지를 한 줄에 넣으면 사람은 한쪽만 답하고,'
275
+ + ' 나머지는 답을 받은 것으로 착각한 채 진행하게 됩니다.'
276
+ + ' 정말 둘 다 필요하면 더 급한 것부터 하나만 묻고, 나머지는 그 답을 받고 나서.',
277
+ };
278
+ }
279
+
280
+ return { ok: true };
281
+ }
@@ -0,0 +1,186 @@
1
+ // 시킨 것을 세고, 턴이 끝날 때 대조한다.
2
+ //
3
+ // ── 무엇을 막나 ────────────────────────────────────────────────────────
4
+ //
5
+ // 「요청사항이 다량의 요청사항일 경우 몇 번 까먹거나, 요청사항이 누락되거나
6
+ // 그런 이슈들이 생겨」
7
+ //
8
+ // 네 가지를 시키면 셋을 하고 끝낸다. 그것도 조용히 — 안 한 것을 안 했다고
9
+ // 말하지 않으니, 사람은 다 된 줄 알고 넘어갔다가 나중에 발견한다. 그때는
10
+ // 이미 그 위에 다른 것을 쌓아 둔 뒤다.
11
+ //
12
+ // 도구 호출을 열댓 번 하는 사이 앞의 것이 밀려나기 때문이다. 컨텍스트가
13
+ // 접히면 더 심해진다 — 요약은 「네 가지를 고쳐 달라고 했다」 로 뭉개지고,
14
+ // 그 네 가지가 무엇이었는지는 사라진다.
15
+ //
16
+ // ── 어떻게 막나 ────────────────────────────────────────────────────────
17
+ //
18
+ // 프롬프트로 "빠뜨리지 마라" 고 적는 것으로는 안 된다. 이미 적혀 있다.
19
+ // 그래서 **턴이 끝날 때 기계가 대조한다.** 시킨 것을 항목으로 쪼개 두고,
20
+ // 끝날 때 그 항목 하나하나가 어디엔가 닿았는지 본다 — 할 일 목록에 있나,
21
+ // 손댄 파일 이름에 있나, 마지막 보고에 있나.
22
+ //
23
+ // 닿은 자국이 없으면 그 항목은 **안 한 것**으로 본다. 그러면 한 번 되민다.
24
+ // 그래도 없으면 사람에게 그대로 말한다. 조용히 넘어가는 것보다 낫다.
25
+ //
26
+ // ── 무엇을 못 하나 ──────────────────────────────────────────────────────
27
+ //
28
+ // 글자만 본다. "정말 제대로 했나" 는 모른다 — 항목 이름이 보고에 적혀 있으면
29
+ // 닿은 것으로 친다. 그러니 이건 **품질 검사가 아니라 누락 검사**다.
30
+ // 애매하면 안 걸고 넘어간다. 다 했는데 안 했다고 하는 것이 더 나쁘다.
31
+ /*
32
+ * 뜻이 없는 말.
33
+ *
34
+ * 이 낱말들이 자국에 보였다고 그 항목을 다룬 것은 아니다. '다른' 은 거의 모든
35
+ * 글에 있고, '문제' 와 '이슈' 는 어떤 보고에도 나온다. 걸러 두지 않으면
36
+ * "안 한 것" 이 하나도 안 잡힌다 — 있으나 마나 한 검사가 된다.
37
+ */
38
+ const 뜻없는말 = new Set([
39
+ '그리고', '그래서', '하지만', '그런데', '그러면', '그냥', '조금', '많이', '자꾸', '아직',
40
+ '이런', '그런', '저런', '이거', '그거', '저거', '여기', '거기', '이게', '그게',
41
+ '해줘', '주세요', '좀더', '같은', '같이', '때문', '경우', '정도', '부분', '다른',
42
+ '문제', '이슈', '생겨', '있어', '없어', '되는', '하는', '해서', '해도', '으로',
43
+ 'the', 'and', 'for', 'with', 'that', 'this', 'issue', 'problem',
44
+ ]);
45
+
46
+ /**
47
+ * 항목에서 알맹이가 되는 낱말만 뽑는다.
48
+ *
49
+ * 두 글자 미만은 버린다 — 한 글자짜리는 어디에나 있다.
50
+ */
51
+ export function 알맹이낱말(글) {
52
+ const 것 = String(글 ?? '').toLowerCase().split(/[^0-9a-z가-힣]+/);
53
+ return [...new Set(것.filter((w) => w.length >= 2 && !뜻없는말.has(w)))];
54
+ }
55
+
56
+ /*
57
+ * 이 낱말이 자국에 보이나.
58
+ *
59
+ * 한국어는 말끝이 붙는다 — '누락되는' 과 '누락된' 과 '누락' 은 같은 말이다.
60
+ * 그래서 뒤에서 한두 글자를 떼고도 본다. 앞에서 떼지는 않는다 — 한국어는
61
+ * 앞이 뿌리다.
62
+ */
63
+ function 낱말이닿았나(낱말, 덩어리) {
64
+ if (덩어리.includes(낱말)) return true;
65
+ /*
66
+ * 한글은 앞에서부터 줄여 가며 본다. 두 글자까지 내려간다.
67
+ *
68
+ * '요청사항이' 와 '요청', '누락되는' 과 '누락' 은 같은 말인데 뒤가 다 다르다.
69
+ * 한 글자만 떼서는 못 만난다 — 실제로 "요청 누락 대조" 라고 적어 둔 일을
70
+ * 안 했다고 말한 적이 있다.
71
+ *
72
+ * 두 글자까지 내려가는 것이 헐거운 줄 안다. 그래서 이쪽으로 틀린다 —
73
+ * 다룬 것으로 잘못 보는 쪽. 안 한 것을 놓치면 예전과 같아질 뿐이지만,
74
+ * 다 한 일을 안 했다고 말하면 사람이 이 말을 안 믿게 된다.
75
+ *
76
+ * 영문은 안 줄인다. cli·deel·npm 처럼 원래 짧아서, 줄이면 아무 데나 걸린다.
77
+ */
78
+ if (!/^[가-힣]/.test(낱말)) return false;
79
+ for (let n = 낱말.length - 1; n >= 2; n--) {
80
+ if (덩어리.includes(낱말.slice(0, n))) return true;
81
+ }
82
+ return false;
83
+ }
84
+
85
+ /*
86
+ * 시킴꼴 끝맺음.
87
+ *
88
+ * 줄마다 시킨 것을 적는 사람이 많다. 번호를 안 붙여도 "…해줘" 로 끝나는 줄이
89
+ * 여럿이면 그건 목록이다.
90
+ */
91
+ const 시킴꼴 = /(줘|주세요|주라|해라|하라|하자|해다오|할래|해야\s*(해|돼|한다)|바랍니다|부탁(해|합니다|드립니다))[\s.!~]*$/;
92
+
93
+ // 줄머리 표. 번호(1. 2))·글머리(- * ·)·동그라미 숫자.
94
+ const 줄머리 = /^\s*(?:(\d{1,2})\s*[.)]|[-*·▪️•]|[①-⑮])\s+/;
95
+
96
+ /**
97
+ * 시킨 것을 항목으로 쪼갠다.
98
+ *
99
+ * 두 가지만 본다 — 번호·글머리를 붙인 줄, 그리고 시킴꼴로 끝나는 줄.
100
+ * 문장 하나를 쉼표로 더 쪼개지는 않는다. 쪼갤수록 없는 항목이 생기고,
101
+ * 없는 항목을 "안 했다" 고 말하는 것이 제일 나쁘다.
102
+ *
103
+ * @returns {Array<{번호:number, 글:string}>}
104
+ */
105
+ export function 요청항목들(요청) {
106
+ const 줄들 = String(요청 ?? '').split('\n').map((l) => l.trimEnd());
107
+
108
+ // 1) 번호·글머리를 붙인 줄
109
+ const 표붙은것 = [];
110
+ for (const 줄 of 줄들) {
111
+ const m = 줄머리.exec(줄);
112
+ if (!m) continue;
113
+ const 글 = 줄.slice(m[0].length).trim();
114
+ if (글) 표붙은것.push(글);
115
+ }
116
+ if (표붙은것.length >= 2) return 표붙은것.map((글, i) => ({ 번호: i + 1, 글 }));
117
+
118
+ // 2) 표는 없지만 줄마다 시키는 꼴
119
+ const 시킨줄 = 줄들.map((l) => l.trim()).filter((l) => l && 시킴꼴.test(l));
120
+ if (시킨줄.length >= 2) return 시킨줄.map((글, i) => ({ 번호: i + 1, 글 }));
121
+
122
+ return [];
123
+ }
124
+
125
+ /*
126
+ * ── 언제 "안 했다" 고 하나 ──────────────────────────────────────────────
127
+ *
128
+ * 처음에는 항목 글 전체가 자국에 얼마나 겹치는지로 쟀다. 안 됐다. 시킨 말은
129
+ * "붙여넣기 해도 창에 붙여넣기된 내용이 다 뜨는 게 아니야" 인데 한 일은
130
+ * "붙여넣기된 내용 표시 고침" 이라, 사이의 이음말이 다 달라서 0.32 밖에
131
+ * 안 나왔다. 문턱을 그 밑으로 내리면 이번에는 아무것도 안 잡힌다.
132
+ *
133
+ * 그래서 **알맹이 낱말이 하나라도 닿았나** 로 바꿨다. 하나도 안 닿았다는 것은
134
+ * 할 일 목록에도, 손댄 파일 이름에도, 마지막 보고에도 그 말이 한 번도 안
135
+ * 나왔다는 뜻이다. 그건 잊은 것이 거의 확실하다.
136
+ *
137
+ * 느슨한 잣대인 것을 알고 고른 것이다. 틀리는 방향이 한쪽으로 심하게 기울기
138
+ * 때문이다 — 안 한 것을 놓치면 예전과 같아질 뿐이지만, **다 한 일을 안 했다고
139
+ * 말하면** 사람은 이 말을 믿지 않게 되고, 그때부터 이 검사는 없느니만 못하다.
140
+ */
141
+
142
+ /**
143
+ * 시킨 것 중 자국이 하나도 없는 항목.
144
+ *
145
+ * @param {object} 것
146
+ * @param {string} 것.요청 사용자가 친 말
147
+ * @param {string[]} [것.자국] 한 일의 자국 — 할 일 목록·손댄 파일·마지막 보고
148
+ * @returns {Array<{번호:number, 글:string, 닿은낱말:number, 낱말수:number}>}
149
+ */
150
+ export function 빠진것({ 요청 = '', 자국 = [] } = {}) {
151
+ const 항목 = 요청항목들(요청);
152
+ // 하나짜리 요청은 대조하지 않는다. 한 가지를 시켰는데 안 했으면 그건
153
+ // 누락이 아니라 그냥 안 한 것이고, 그건 다른 자리(되밀기)가 본다.
154
+ if (항목.length < 2) return [];
155
+
156
+ const 덩어리 = (Array.isArray(자국) ? 자국 : [자국])
157
+ .filter(Boolean).join('\n').toLowerCase();
158
+
159
+ const 빠진 = [];
160
+ for (const x of 항목) {
161
+ const 낱말 = 알맹이낱말(x.글);
162
+ // 알맹이가 하나도 없는 항목("그리고 그것도")은 잴 수가 없다. 안 건다.
163
+ if (!낱말.length) continue;
164
+ const 닿은낱말 = 낱말.filter((w) => 낱말이닿았나(w, 덩어리)).length;
165
+ if (닿은낱말 === 0) 빠진.push({ ...x, 닿은낱말, 낱말수: 낱말.length });
166
+ }
167
+ return 빠진;
168
+ }
169
+
170
+ /**
171
+ * 모델에게 되밀 때 하는 말.
172
+ *
173
+ * 무엇이 빠졌는지 **그대로 다시 적어 준다.** "빠뜨린 것이 있습니다" 만으로는
174
+ * 모델이 무엇을 빠뜨렸는지 스스로 못 찾는다 — 못 찾으니까 빠뜨린 것이다.
175
+ */
176
+ export function 빠졌다는말(빠진, { 영어 = false } = {}) {
177
+ const 목록 = 빠진.map((x) => ` ${x.번호}. ${x.글}`).join('\n');
178
+ if (영어) {
179
+ return `You have not addressed ${빠진.length} of the things that were asked:\n${목록}\n`
180
+ + 'Do them now. If one of them genuinely cannot be done, say in one line why —'
181
+ + ' do not finish while leaving it unmentioned.';
182
+ }
183
+ return `시킨 것 중 ${빠진.length}가지에 아직 손을 안 댔습니다:\n${목록}\n`
184
+ + '지금 하세요. 정말 못 하는 것이면 왜 못 하는지 한 줄로 밝히세요 —'
185
+ + ' 말없이 빼놓고 끝내지 마세요.';
186
+ }
@@ -169,6 +169,31 @@ const 요약지시 = `지금까지의 대화를 다음 형식으로 요약하세
169
169
  // 쓰던 자리가 있으니 이름은 그대로 내보낸다.
170
170
  export { safeCut, safeHead } from './session.js';
171
171
 
172
+ /*
173
+ * ── 시킨 말은 요약하지 않는다 ───────────────────────────────────────────
174
+ *
175
+ * 요약 지시에는 「## 목표 — 사용자가 무엇을 시켰는가」 가 있다. 그런데 요약은
176
+ * 요약이다. 네 가지를 적어 준 요청이 「네 가지를 고쳐 달라고 했다」 한 줄로
177
+ * 뭉개지고, **그 네 가지가 무엇이었는지는 사라진다.** 접힌 뒤로는 아무리
178
+ * 찾아도 원문이 없으니, 남은 것을 이어 하려 해도 무엇이 남았는지 모른다.
179
+ *
180
+ * "요청사항이 다량일 경우 몇 번 까먹거나 누락되거나" 의 절반이 여기였다.
181
+ *
182
+ * 그래서 이번에 시킨 말을 **글자 그대로** 한 번 더 박아 둔다. 요약 모델이
183
+ * 무엇을 하든 이 줄은 그대로 남는다. 길면 앞부분만 — 그래도 항목 목록은
184
+ * 대개 앞에 있다.
185
+ */
186
+ const 못박을길이 = 1200;
187
+
188
+ export function 못박은요청(session) {
189
+ const 원문 = String(session?.이번요청 ?? '').trim();
190
+ if (!원문) return '';
191
+ const 실을것 = 원문.length > 못박을길이
192
+ ? `${원문.slice(0, 못박을길이)}\n…(뒷부분 줄임)`
193
+ : 원문;
194
+ return `[이번에 시킨 말 — 요약이 아니라 원문 그대로입니다. 여기 적힌 것을 빠짐없이 하세요.]\n${실을것}\n\n`;
195
+ }
196
+
172
197
  /** 접을 구간과 남길 구간을 나눈다. */
173
198
  export function split(messages, { keepHead = KEEP_HEAD, tailRatio = KEEP_TAIL_RATIO } = {}) {
174
199
  const n = messages.length;
@@ -252,7 +277,9 @@ export async function compact(session, { auto = false, signal = null, onBackoff
252
277
  ...parts.head,
253
278
  {
254
279
  role: 'user',
255
- content: `[앞선 대화 ${parts.fold.length}개를 요약해 접었습니다. 아래가 그 요약입니다.]\n\n${summary}\n\n[요약 끝. 이어서 진행하세요. 파일 내용이 필요하면 다시 읽으세요.]`,
280
+ content: `[앞선 대화 ${parts.fold.length}개를 요약해 접었습니다. 아래가 그 요약입니다.]\n\n${summary}\n\n`
281
+ + 못박은요청(session)
282
+ + '[요약 끝. 이어서 진행하세요. 파일 내용이 필요하면 다시 읽으세요.]',
256
283
  },
257
284
  ...parts.tail,
258
285
  ];