deel-local-cli 2.0.1 → 2.1.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 (72) hide show
  1. package/README.ko.md +34 -7
  2. package/README.md +35 -7
  3. package/bin/deel.js +34 -1
  4. package/package.json +1 -1
  5. package/src/acp/serve.js +83 -17
  6. package/src/agent/commit.js +2 -2
  7. package/src/agent/compact.js +4 -4
  8. package/src/agent/donecheck.js +97 -0
  9. package/src/agent/grade.js +1 -1
  10. package/src/agent/loop.js +176 -13
  11. package/src/agent/memory.js +1 -1
  12. package/src/agent/models.js +1 -1
  13. package/src/agent/modes.js +3 -0
  14. package/src/agent/outschema.js +1 -1
  15. package/src/agent/pins.js +7 -2
  16. package/src/agent/project.js +1 -1
  17. package/src/agent/recall.js +1 -1
  18. package/src/agent/session.js +15 -0
  19. package/src/agent/store.js +13 -26
  20. package/src/backend/ctxsize.js +2 -2
  21. package/src/backend/http.js +1 -1
  22. package/src/backend/learn.js +27 -1
  23. package/src/backend/mcp.js +97 -16
  24. package/src/backend/proxy.js +1 -1
  25. package/src/backend/quota.js +2 -2
  26. package/src/commands.js +24 -2
  27. package/src/completion.js +16 -3
  28. package/src/config.js +33 -11
  29. package/src/configexplain.js +1 -1
  30. package/src/doctor.js +11 -1
  31. package/src/eval/run.js +432 -0
  32. package/src/eval/starter.js +95 -0
  33. package/src/i18n/en.js +20 -0
  34. package/src/i18n/ja.js +20 -0
  35. package/src/i18n/ko.js +20 -0
  36. package/src/i18n/zh.js +20 -0
  37. package/src/jsonl.js +42 -0
  38. package/src/lsp/client.js +6 -46
  39. package/src/oneshot.js +85 -3
  40. package/src/pack/selfpack.js +2 -2
  41. package/src/pack/zip.js +18 -1
  42. package/src/plugins/manage.js +81 -14
  43. package/src/reap.js +70 -0
  44. package/src/repl.js +85 -11
  45. package/src/report.js +1 -1
  46. package/src/safety/audit.js +4 -19
  47. package/src/safety/authcmd.js +1 -1
  48. package/src/safety/guard.js +56 -6
  49. package/src/safety/hooks.js +1 -1
  50. package/src/safety/keystore.js +18 -3
  51. package/src/safety/policy.js +38 -0
  52. package/src/safety/shellenv.js +1 -1
  53. package/src/safety/undo.js +44 -20
  54. package/src/skills/discover.js +1 -1
  55. package/src/tools/docs.js +3 -3
  56. package/src/tools/edit-match.js +1 -1
  57. package/src/tools/encoding.js +1 -1
  58. package/src/tools/excel-com.js +56 -30
  59. package/src/tools/fastgrep.js +1 -1
  60. package/src/tools/fsutil.js +8 -4
  61. package/src/tools/ignore.js +21 -1
  62. package/src/tools/index.js +122 -11
  63. package/src/tools/jobs.js +23 -4
  64. package/src/tools/shell.js +27 -0
  65. package/src/tools/todo.js +1 -1
  66. package/src/tools/webfetch.js +1 -1
  67. package/src/tools/xlsx.js +4 -4
  68. package/src/ui/approve.js +57 -3
  69. package/src/ui/banner.js +1 -1
  70. package/src/ui/inputbox.js +1 -1
  71. package/src/ui/md.js +1 -1
  72. package/src/ui/motion.js +1 -1
@@ -0,0 +1,432 @@
1
+ // `deel eval` — 과제 모음(골든셋)을 실제 모델로 돌려 **얼마나 해내나**를 잰다 (2.1.0).
2
+ //
3
+ // ── 왜 만드나 ───────────────────────────────────────────────────────────
4
+ //
5
+ // 사내 검토에서 들은 말이다 — 「검증 조건에서 작업마다 골든셋이 필요하다. 지금처럼
6
+ // 정적 검사에 기대면 한계가 있다.」 맞는 말이었다. 검사 1만 4천여 개는 가짜 게이트웨이로
7
+ // 「도구가 약속대로 도나」 를 잰다. 「이 회사의 모델과 deel 이 실제 일을 몇 개 해내나」 는
8
+ // 한 번도 안 쟀다. 모델을 바꾸거나 deel 을 올릴 때 나아졌는지 나빠졌는지 말할 숫자가 없었다.
9
+ //
10
+ // ── 어떻게 재나 ─────────────────────────────────────────────────────────
11
+ //
12
+ // 과제 하나는 폴더 하나다.
13
+ //
14
+ // 과제이름/
15
+ // task.json { "prompt": 시킬 말, "check": 정답 검사 명령, … }
16
+ // start/ 시작 파일 — 임시 작업 폴더로 복사된다
17
+ // golden/ 정답 검사 파일 — deel 이 **다 끝낸 뒤에** 작업 폴더에 들어간다
18
+ //
19
+ // 1. 임시 폴더에 start/ 를 깐다
20
+ // 2. 그 폴더에서 `deel run --json --yes "<prompt>"` 를 돌린다 — 사람이 쓰는 그 길 그대로
21
+ // 3. golden/ 을 덮어 넣고 check 를 돌린다. 종료코드 0 이면 통과
22
+ //
23
+ // 정답은 **모델이 못 본다.** 일하는 동안 폴더에 없다가 끝난 뒤에 들어온다. 그래서 「검사를
24
+ // 통과하게 검사를 고치는」 길이 없다. 모델이 같은 이름의 파일을 만들어 둬도 덮인다.
25
+ //
26
+ // 결과는 `<과제 모음>/.results/<시각>.json` 에 남고, 지난 결과와 견줘 **나빠진 과제**를
27
+ // 짚는다. 판을 올리거나 모델을 바꾼 뒤 한 번 돌려 보는 것이 쓰임새다.
28
+ //
29
+ // ── 안 하는 것 ──────────────────────────────────────────────────────────
30
+ //
31
+ // 동시에 안 돌린다. 과제마다 모델을 오래 부르고 검사가 CPU 를 먹는다 — 사내 게이트웨이의
32
+ // 분당 한도와 이 PC 의 메모리를 같이 지킨다. 느려도 숫자가 맞는 쪽을 고른다.
33
+ //
34
+ // check 는 **사람이 적은 명령**이라 관문 없이 돈다(npm test 를 손으로 치는 것과 같다).
35
+ // 다만 그 명령이 모델이 쓴 코드를 돌리므로 열쇠처럼 생긴 환경변수는 빼고 넘긴다
36
+ // (safety/shellenv.js). 남이 준 과제 모음은 task.json 을 먼저 읽어 보고 돌리라.
37
+ import { existsSync, readdirSync, readFileSync, statSync, mkdirSync, writeFileSync, copyFileSync, mkdtempSync, rmSync } from 'node:fs';
38
+ import { join, dirname, resolve, relative } from 'node:path';
39
+ import { tmpdir } from 'node:os';
40
+ import { fileURLToPath } from 'node:url';
41
+ import { 돌려보기 } from '../tools/spawn.js';
42
+ import { 셸명령 } from '../tools/shell.js';
43
+ import { 셸환경 } from '../safety/shellenv.js';
44
+ import { 승인바닥 } from '../safety/policy.js';
45
+ import { c, say, mark } from '../ui/ansi.js';
46
+ import { 언어 } from '../i18n/index.js';
47
+ import { 예제과제 } from './starter.js';
48
+
49
+ const 진입점 = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'bin', 'deel.js');
50
+
51
+ /** 과제 하나를 deel 이 끝낼 때까지 기다리는 시간(초)과, 정답 검사 한 번의 시간(초). */
52
+ export const 기본시간초 = 600;
53
+ export const 검사시간초 = 120;
54
+ /** 한 과제를 몇 번까지 되풀이해 재나. 모델은 같은 말에도 다르게 답한다. */
55
+ export const 최대되풀이 = 20;
56
+ export const 결과폴더 = '.results';
57
+
58
+ const 글 = (ko, en) => (언어() === 'ko' ? ko : en);
59
+
60
+ /**
61
+ * 과제 모음 폴더를 읽는다. task.json 이 있는 하위 폴더 하나가 과제 하나다.
62
+ * 모양이 틀린 과제는 **빼지 않고** 탈을 달아 돌려준다 — 조용히 빼면 스무 개 중 열아홉만
63
+ * 돌고 「다 통과」 가 된다.
64
+ *
65
+ * @returns {Array<{이름:string, 자리:string, 설정:object|null, 탈:string|null}>}
66
+ */
67
+ export function 과제읽기(폴더) {
68
+ const 과제들 = [];
69
+ for (const 이름 of readdirSync(폴더).sort()) {
70
+ if (이름.startsWith('.')) continue;
71
+ const 자리 = join(폴더, 이름);
72
+ let 폴더인가 = false;
73
+ try { 폴더인가 = statSync(자리).isDirectory(); } catch { /* 사이에 지워졌다 */ }
74
+ if (!폴더인가) continue;
75
+ /*
76
+ * task.json 이 없어도 start/·golden/ 이 있으면 과제로 만든 폴더다 — 이름을 틀렸거나
77
+ * (`Task.json` · `task.jsn`) 빠뜨린 것이다. 이것도 탈로 센다. 둘 다 없는 폴더(과제들이
78
+ * 같이 쓰는 도우미 따위)만 과제가 아니다 (2차 눈 판정).
79
+ */
80
+ if (!existsSync(join(자리, 'task.json'))) {
81
+ if (existsSync(join(자리, 'start')) || existsSync(join(자리, 'golden'))) {
82
+ 과제들.push({ 이름, 자리, 설정: null, 탈: 'task.json 이 없습니다 — start/·golden/ 은 있는데 시킬 말과 정답 검사가 없습니다' });
83
+ }
84
+ continue;
85
+ }
86
+ let 설정 = null;
87
+ let 탈 = null;
88
+ try {
89
+ const v = JSON.parse(readFileSync(join(자리, 'task.json'), 'utf8').replace(/^\uFEFF/, ''));
90
+ 탈 = 설정탈(v);
91
+ if (!탈) 설정 = v;
92
+ } catch (e) {
93
+ 탈 = `task.json 을 못 읽었습니다 — ${String(e?.message ?? e).split('\n')[0]}`;
94
+ }
95
+ 과제들.push({ 이름, 자리, 설정, 탈 });
96
+ }
97
+ return 과제들;
98
+ }
99
+
100
+ /** task.json 이 쓸 수 있는 모양인가. 쓸 수 있으면 null, 아니면 까닭. */
101
+ export function 설정탈(v) {
102
+ if (!v || typeof v !== 'object' || Array.isArray(v)) return 'task.json 이 객체가 아닙니다';
103
+ if (typeof v.prompt !== 'string' || !v.prompt.trim()) return 'prompt(시킬 말)가 없습니다';
104
+ if (typeof v.check !== 'string' || !v.check.trim()) return 'check(정답 검사 명령)가 없습니다';
105
+ for (const 칸 of ['timeout', 'checkTimeout']) {
106
+ if (v[칸] !== undefined && !(Number.isFinite(Number(v[칸])) && Number(v[칸]) > 0)) return `${칸} 은 0 보다 큰 초여야 합니다`;
107
+ }
108
+ for (const 칸 of ['doneCheck', 'work']) {
109
+ if (v[칸] !== undefined && typeof v[칸] !== 'string') return `${칸} 은 글이어야 합니다`;
110
+ }
111
+ return null;
112
+ }
113
+
114
+ /**
115
+ * 폴더를 통째로 복사한다. 원본이 없으면 아무것도 안 한다(start/ 가 없는 과제는 빈 폴더에서 시작).
116
+ *
117
+ * `덮기` 는 정답을 넣을 때 쓴다. 모델이 같은 이름을 **다른 꼴**로 만들어 두었으면(정답은
118
+ * `sub/` 폴더인데 모델은 `sub` 파일) 그것을 걷어내고 정답을 넣는다. 안 걷으면 복사가 터져
119
+ * 평가 전체가 죽는다 — 과제 하나의 일로 스무 개가 다 멈춘다 (2차 눈 판정).
120
+ */
121
+ export function 폴더복사(원본, 대상, { 덮기 = false } = {}) {
122
+ if (!existsSync(원본)) return 0;
123
+ let n = 0;
124
+ for (const 이름 of readdirSync(원본)) {
125
+ const 가 = join(원본, 이름);
126
+ const 나 = join(대상, 이름);
127
+ const 폴더다 = statSync(가).isDirectory();
128
+ if (덮기) {
129
+ let 있는것 = null;
130
+ try { 있는것 = statSync(나); } catch { /* 없다 */ }
131
+ if (있는것 && 있는것.isDirectory() !== 폴더다) rmSync(나, { recursive: true, force: true, maxRetries: 3 });
132
+ }
133
+ if (폴더다) { mkdirSync(나, { recursive: true }); n += 폴더복사(가, 나, { 덮기 }); }
134
+ else { mkdirSync(dirname(나), { recursive: true }); copyFileSync(가, 나); n++; }
135
+ }
136
+ return n;
137
+ }
138
+
139
+ /**
140
+ * 예제 과제를 만든다. 이미 있는 과제 폴더는 **안 덮는다** — 사람이 고쳐 둔 과제를 지우면 안 된다.
141
+ * @returns {{만든:string[], 건너뜀:string[]}}
142
+ */
143
+ export function 예제만들기(폴더) {
144
+ const 만든 = [];
145
+ const 건너뜀 = [];
146
+ for (const [이름, 과제] of Object.entries(예제과제)) {
147
+ const 자리 = join(폴더, 이름);
148
+ if (existsSync(자리)) { 건너뜀.push(이름); continue; }
149
+ mkdirSync(자리, { recursive: true });
150
+ writeFileSync(join(자리, 'task.json'), JSON.stringify(과제.task, null, 2) + '\n');
151
+ for (const [갈래, 파일들] of [['start', 과제.start], ['golden', 과제.golden]]) {
152
+ for (const [길, 내용] of Object.entries(파일들 ?? {})) {
153
+ const 어디 = join(자리, 갈래, 길);
154
+ mkdirSync(dirname(어디), { recursive: true });
155
+ writeFileSync(어디, 내용);
156
+ }
157
+ }
158
+ 만든.push(이름);
159
+ }
160
+ return { 만든, 건너뜀 };
161
+ }
162
+
163
+ /** `deel run --json` 의 표준출력에서 마지막 JSON 한 덩이를 읽는다. 못 읽으면 null. */
164
+ export function 결과덩이(stdout) {
165
+ const 줄들 = String(stdout ?? '').trim().split('\n').reverse();
166
+ for (const 줄 of 줄들) {
167
+ const s = 줄.trim();
168
+ if (!s.startsWith('{')) continue;
169
+ try { return JSON.parse(s); } catch { /* 다음 줄 */ }
170
+ }
171
+ return null;
172
+ }
173
+
174
+ const 꼬리 = (s, n = 800) => {
175
+ const t = String(s ?? '').trim();
176
+ return t.length > n ? `…${t.slice(t.length - n)}` : t;
177
+ };
178
+
179
+ /**
180
+ * 과제 하나를 한 번 돌린다.
181
+ *
182
+ * 통과·실패는 **정답 검사 하나로만** 가른다. deel 이 걸음 상한이나 오류로 끝났어도 폴더가
183
+ * 맞게 고쳐져 있으면 통과다 — 재는 것은 「해냈나」 지 「깔끔하게 끝냈나」 가 아니다.
184
+ * deel 이 어떻게 끝났는지는 옆에 적는다.
185
+ */
186
+ export async function 한번돌리기(과제, { 판 = 1, 깃발들 = [], 남김 = false, 시간초 = null } = {}) {
187
+ const 설정 = 과제.설정;
188
+ const t0 = Date.now();
189
+ const 작업 = mkdtempSync(join(tmpdir(), 'deel-eval-'));
190
+ try {
191
+ 폴더복사(join(과제.자리, 'start'), 작업);
192
+ const 제한초 = Number(설정.timeout ?? 시간초 ?? 기본시간초);
193
+ const 인자 = [
194
+ 진입점, 'run', '--json', '--yes', '--root', 작업,
195
+ ...(설정.work ? ['--work', 설정.work] : []),
196
+ ...(설정.doneCheck ? ['--check', 설정.doneCheck] : []),
197
+ ...깃발들,
198
+ // `--` 뒤는 낱말이다 — 시킬 말이 대시로 시작해도 깃발로 안 읽힌다.
199
+ '--', 설정.prompt,
200
+ ];
201
+ const 돌림 = await 돌려보기(process.execPath, 인자, {
202
+ timeout: 제한초 * 1000, maxBuffer: 64 * 1024 * 1024,
203
+ 덤: { cwd: 작업, env: { ...process.env, NO_COLOR: '1' } },
204
+ });
205
+ const 덩이 = 결과덩이(돌림.stdout);
206
+ const deel = {
207
+ code: 돌림.status,
208
+ reason: 덩이?.reason ?? (돌림.error ? (/안 끝났습니다/.test(돌림.error.message) ? 'timeout' : 'spawn') : 'no-json'),
209
+ steps: 덩이?.steps ?? 0,
210
+ tools: 덩이?.tools ?? 0,
211
+ usage: { in: 덩이?.usage?.prompt ?? 덩이?.usage?.in ?? 0, out: 덩이?.usage?.out ?? 0 },
212
+ model: 덩이?.model ?? null,
213
+ ...(덩이?.check ? { check: 덩이.check } : {}),
214
+ ...(덩이 ? {} : { stderr: 꼬리(돌림.stderr) }),
215
+ };
216
+
217
+ // 정답을 넣는다 — 일이 다 끝난 **뒤에**. 모델이 같은 이름으로 만든 것은 꼴이 달라도 덮인다.
218
+ 폴더복사(join(과제.자리, 'golden'), 작업, { 덮기: true });
219
+ const 셸 = 셸명령(설정.check);
220
+ const 검사 = await 돌려보기(셸.file, 셸.args, {
221
+ timeout: Number(설정.checkTimeout ?? 검사시간초) * 1000,
222
+ 덤: { cwd: 작업, env: 셸환경(process.env).env, windowsVerbatimArguments: 셸.verbatim },
223
+ });
224
+ const 통과 = !검사.error && 검사.status === 0;
225
+ return {
226
+ 과제: 과제.이름, 판, 통과,
227
+ 초: Math.round((Date.now() - t0) / 100) / 10,
228
+ deel,
229
+ 검사: { code: 검사.status, ...(통과 ? {} : { 꼬리: 꼬리(`${검사.stdout ?? ''}\n${검사.stderr ?? ''}\n${검사.error?.message ?? ''}`) }) },
230
+ ...(남김 ? { 작업폴더: 작업 } : {}),
231
+ };
232
+ } finally {
233
+ if (!남김) {
234
+ try { rmSync(작업, { recursive: true, force: true, maxRetries: 3 }); } catch { /* 임시 폴더다 — 못 지워도 OS 가 거둔다 */ }
235
+ }
236
+ }
237
+ }
238
+
239
+ /** 과제별로 묶는다 — 몇 판 중 몇 판 통과했나. */
240
+ export function 과제별(결과들) {
241
+ const 표 = new Map();
242
+ for (const r of 결과들) {
243
+ const 것 = 표.get(r.과제) ?? { 과제: r.과제, 판: 0, 통과: 0 };
244
+ 것.판++;
245
+ if (r.통과) 것.통과++;
246
+ 표.set(r.과제, 것);
247
+ }
248
+ return [...표.values()];
249
+ }
250
+
251
+ /**
252
+ * 지난 결과와 견준다. 다 통과하던 과제가 하나라도 떨어졌으면 나빠짐, 못 하던 과제를 다 해내면 나아짐.
253
+ * 되풀이 수가 다르면 비율로 본다.
254
+ */
255
+ export function 견주기(지난것, 지금것) {
256
+ const 비율 = (x) => (x.판 ? x.통과 / x.판 : 0);
257
+ const 지난표 = new Map((지난것 ?? []).map((x) => [x.과제, x]));
258
+ const 나빠짐 = [];
259
+ const 나아짐 = [];
260
+ for (const x of 지금것) {
261
+ const 전 = 지난표.get(x.과제);
262
+ if (!전) continue;
263
+ if (비율(x) < 비율(전)) 나빠짐.push(x.과제);
264
+ else if (비율(x) > 비율(전)) 나아짐.push(x.과제);
265
+ }
266
+ return { 나빠짐, 나아짐 };
267
+ }
268
+
269
+ /** 제일 새 결과 파일. 깨졌거나 과제별이 없는 것(손으로 적은 `{}` 따위)은 건너뛴다 — 견줄 것이 없다. */
270
+ export function 지난결과(폴더) {
271
+ const 자리 = join(폴더, 결과폴더);
272
+ if (!existsSync(자리)) return null;
273
+ const 파일들 = readdirSync(자리).filter((f) => /^\d{8}-\d{6}.*\.json$/.test(f)).sort();
274
+ for (const f of 파일들.reverse()) {
275
+ try {
276
+ const 값 = JSON.parse(readFileSync(join(자리, f), 'utf8'));
277
+ if (Array.isArray(값?.과제별)) return { 파일: f, 값 };
278
+ } catch { /* 깨진 것은 건너뛴다 */ }
279
+ }
280
+ return null;
281
+ }
282
+
283
+ /**
284
+ * 결과 파일 이름. **UTC** 로 적고 `Z` 를 붙인다 — 지역 시각이면 시차가 다른 PC 의 결과가 한
285
+ * 폴더에 섞일 때(저장소에 올려 같이 쓰는 과제 모음) 이름 순서가 시각 순서와 어긋나고,
286
+ * 「지난번」 을 엉뚱한 파일로 고른다 (2차 눈 판정).
287
+ */
288
+ export const 시각이름 = (d = new Date()) => {
289
+ const p = (n) => String(n).padStart(2, '0');
290
+ return `${d.getUTCFullYear()}${p(d.getUTCMonth() + 1)}${p(d.getUTCDate())}-${p(d.getUTCHours())}${p(d.getUTCMinutes())}${p(d.getUTCSeconds())}Z`;
291
+ };
292
+
293
+ /**
294
+ * `deel eval [폴더]` 의 몸통. 종료코드를 돌려준다 — 0 다 통과 · 1 하나라도 실패 · 64 부른 모양이 틀림.
295
+ */
296
+ export async function runEval({ 폴더 = null, init = false, repeat = 1, keep = false, json = false, only = null, 깃발들 = [], 시간초 = null } = {}) {
297
+ const 뿌리 = process.cwd();
298
+ const 고른폴더 = resolve(뿌리, 폴더 ?? (existsSync(join(뿌리, 'golden')) || init ? 'golden' : join('.deel', 'golden')));
299
+ const 보일 = relative(뿌리, 고른폴더) || '.';
300
+ const 말하기 = (s = '') => { if (!json) say(s); };
301
+ /*
302
+ * 부른 모양이 틀렸거나 돌릴 것이 없을 때. `--json` 이면 받는 쪽은 표준출력을 통째로
303
+ * JSON.parse 한다 — 색 입힌 글을 내면 그 자리에서 깨진다. 그래서 JSON 한 줄로 낸다.
304
+ */
305
+ const 못돌림 = (코드, 오류, 덧 = null) => {
306
+ if (json) process.stdout.write(JSON.stringify({ ok: false, code: 코드, error: 오류 }) + '\n');
307
+ else {
308
+ say(` ${c.red('✗')} ${오류}`);
309
+ if (덧) say(` ${덧}`);
310
+ }
311
+ return 코드;
312
+ };
313
+
314
+ if (init) {
315
+ const { 만든, 건너뜀 } = 예제만들기(고른폴더);
316
+ 말하기('');
317
+ 말하기(` ${mark.ok} ${글(`예제 과제 ${만든.length}개를 만들었습니다 — ${보일}`, `Created ${만든.length} example tasks — ${보일}`)}`);
318
+ for (const 이름 of 만든) 말하기(` ${c.gray(이름)}`);
319
+ if (건너뜀.length) 말하기(` ${c.gray(글(`이미 있어 안 덮은 것: ${건너뜀.join(' · ')}`, `Already there, left alone: ${건너뜀.join(' · ')}`))}`);
320
+ 말하기('');
321
+ 말하기(` ${c.gray(글('돌리기:', 'Run them:'))} ${c.cyan(`deel eval ${보일}`)}`);
322
+ 말하기(` ${c.gray(글('과제 하나 = task.json(시킬 말·정답 검사) + start/(시작 파일) + golden/(끝난 뒤 넣는 정답 검사)',
323
+ 'One task = task.json (prompt, check) + start/ (starting files) + golden/ (checks added after the run)'))}`);
324
+ 말하기('');
325
+ if (json) process.stdout.write(JSON.stringify({ ok: true, folder: 고른폴더, created: 만든, skipped: 건너뜀 }) + '\n');
326
+ return 0;
327
+ }
328
+
329
+ const 되풀이 = Number(repeat);
330
+ if (!Number.isInteger(되풀이) || 되풀이 < 1 || 되풀이 > 최대되풀이) {
331
+ return 못돌림(64, 글(`--repeat 는 1 ~ ${최대되풀이} 사이의 정수여야 합니다: ${repeat}`, `--repeat must be a whole number from 1 to ${최대되풀이}: ${repeat}`));
332
+ }
333
+ if (!existsSync(고른폴더)) {
334
+ return 못돌림(1, 글(`과제 모음이 없습니다 — ${보일}`, `No task folder — ${보일}`),
335
+ `${c.gray(글('예제부터 만들려면:', 'To start from examples:'))} ${c.cyan('deel eval --init')}`);
336
+ }
337
+
338
+ let 과제들 = 과제읽기(고른폴더);
339
+ if (only) {
340
+ const 고를것 = String(only).split(',').map((s) => s.trim()).filter(Boolean);
341
+ 과제들 = 과제들.filter((x) => 고를것.some((k) => x.이름.includes(k)));
342
+ }
343
+ if (!과제들.length) {
344
+ return 못돌림(1, 글(`돌릴 과제가 없습니다 — ${보일} 밑에 task.json 이 든 폴더가 없습니다`, `Nothing to run — no folder with a task.json under ${보일}`));
345
+ }
346
+
347
+ 말하기('');
348
+ 말하기(` ${c.bold(글('골든셋', 'Golden set'))} ${c.gray(`— ${보일} · ${글(`과제 ${과제들.length}개 · 과제마다 ${되풀이}번`, `${과제들.length} tasks · ${되풀이} run(s) each`)}`)}`);
349
+ 말하기('');
350
+
351
+ /*
352
+ * 관리 정책이 승인 바닥을 걸었으면 과제마다 띄우는 `deel run --yes` 의 --yes 가 안 먹는다(oneshot.js).
353
+ * 그러면 파일을 바꾸는 과제는 전부 거절되어 실패로 세어진다 — 모델 탓처럼 보이는 0% 가 나온다. 먼저 말한다.
354
+ */
355
+ const 바닥 = 승인바닥();
356
+ if (바닥.바닥 !== 'auto') {
357
+ 말하기(` ${c.yellow('⚠')} ${글(`관리 정책이 승인을 ${바닥.바닥} 로 걸어 두어 과제 안에서 파일을 못 바꿉니다 — 바꾸는 과제는 실패로 세어집니다 (${바닥.곳})`,
358
+ `A managed policy holds approval at ${바닥.바닥}, so tasks cannot change files — those will count as failed (${바닥.곳})`)}`);
359
+ 말하기('');
360
+ }
361
+ const t0 = Date.now();
362
+ const 결과들 = [];
363
+ for (const 과제 of 과제들) {
364
+ for (let 판 = 1; 판 <= 되풀이; 판++) {
365
+ const 이름표 = 되풀이 > 1 ? `${과제.이름} #${판}` : 과제.이름;
366
+ /*
367
+ * 모양이 틀린 과제도 **되풀이 수만큼** 실패를 센다. 한 판만 세면 `--repeat 5` 에서 분모가
368
+ * 넷 줄어 통과율이 부푼다 — 멀쩡한 과제 5/5 와 깨진 과제 하나가 50% 가 아니라 83% 가 된다
369
+ * (2차 눈 판정). 화면에는 한 번만 적는다.
370
+ */
371
+ if (과제.탈) {
372
+ 결과들.push({ 과제: 과제.이름, 판, 통과: false, 초: 0, 과제탈: 과제.탈 });
373
+ if (판 === 1) 말하기(` ${c.yellow('⊘')} ${과제.이름} ${c.gray(과제.탈)}`);
374
+ continue;
375
+ }
376
+ const r = await 한번돌리기(과제, { 판, 깃발들, 남김: keep, 시간초 });
377
+ 결과들.push(r);
378
+ const 옆 = [`${r.초}${글('초', 's')}`];
379
+ if (r.deel.tools) 옆.push(글(`도구 ${r.deel.tools}회`, `${r.deel.tools} tool calls`));
380
+ if (r.deel.reason && r.deel.reason !== 'done') 옆.push(`deel: ${r.deel.reason}`);
381
+ 말하기(` ${r.통과 ? mark.ok : c.red('✗')} ${이름표} ${c.gray(옆.join(' · '))}`);
382
+ if (!r.통과) {
383
+ for (const 줄 of String(r.검사.꼬리 ?? '').split('\n').filter((l) => l.trim()).slice(-4)) 말하기(` ${c.gray(줄.slice(0, 160))}`);
384
+ if (r.deel.stderr) for (const 줄 of r.deel.stderr.split('\n').filter((l) => l.trim()).slice(-3)) 말하기(` ${c.gray(줄.slice(0, 160))}`);
385
+ }
386
+ if (r.작업폴더) 말하기(` ${c.gray(글(`작업 폴더를 남겼습니다: ${r.작업폴더}`, `Kept the work folder: ${r.작업폴더}`))}`);
387
+ }
388
+ }
389
+
390
+ const 묶음 = 과제별(결과들);
391
+ const 판수 = 결과들.length;
392
+ const 통과수 = 결과들.filter((r) => r.통과).length;
393
+ const 모델 = 결과들.find((r) => r.deel?.model)?.deel.model ?? null;
394
+ const 지난 = 지난결과(고른폴더);
395
+ const 견줌 = 견주기(지난?.값?.과제별, 묶음);
396
+ const 초 = Math.round((Date.now() - t0) / 1000);
397
+ const 토큰 = 결과들.reduce((a, r) => ({ in: a.in + (r.deel?.usage?.in ?? 0), out: a.out + (r.deel?.usage?.out ?? 0) }), { in: 0, out: 0 });
398
+
399
+ const 기록 = {
400
+ when: new Date().toISOString(),
401
+ model: 모델,
402
+ folder: 고른폴더,
403
+ repeat: 되풀이,
404
+ passed: 통과수,
405
+ runs: 판수,
406
+ rate: 판수 ? Math.round((통과수 / 판수) * 1000) / 10 : 0,
407
+ seconds: 초,
408
+ tokens: 토큰,
409
+ 과제별: 묶음,
410
+ 결과: 결과들,
411
+ ...(지난 ? { 견줌: { 지난파일: 지난.파일, ...견줌 } } : {}),
412
+ };
413
+ let 남긴곳 = null;
414
+ try {
415
+ mkdirSync(join(고른폴더, 결과폴더), { recursive: true });
416
+ 남긴곳 = join(고른폴더, 결과폴더, `${시각이름()}.json`);
417
+ writeFileSync(남긴곳, JSON.stringify(기록, null, 2) + '\n');
418
+ } catch { 남긴곳 = null; }
419
+
420
+ if (json) process.stdout.write(JSON.stringify(기록) + '\n');
421
+ 말하기('');
422
+ 말하기(` ${c.gray('──')} ${통과수 === 판수 ? c.green(글(`통과 ${통과수}/${판수}`, `passed ${통과수}/${판수}`)) : c.yellow(글(`통과 ${통과수}/${판수}`, `passed ${통과수}/${판수}`))}`
423
+ + ` ${c.gray(`(${기록.rate}%) · ${초}${글('초', 's')} · ↑${토큰.in.toLocaleString()} ↓${토큰.out.toLocaleString()}${모델 ? ` · ${모델}` : ''}`)}`);
424
+ if (지난) {
425
+ if (견줌.나빠짐.length) 말하기(` ${c.red('▼')} ${글('지난번보다 나빠짐:', 'Worse than last time:')} ${견줌.나빠짐.join(' · ')}`);
426
+ if (견줌.나아짐.length) 말하기(` ${c.green('▲')} ${글('지난번보다 나아짐:', 'Better than last time:')} ${견줌.나아짐.join(' · ')}`);
427
+ if (!견줌.나빠짐.length && !견줌.나아짐.length) 말하기(` ${c.gray(글(`지난번(${지난.파일})과 같습니다`, `Same as last time (${지난.파일})`))}`);
428
+ }
429
+ if (남긴곳) 말하기(` ${c.gray(글(`결과: ${relative(뿌리, 남긴곳)}`, `Results: ${relative(뿌리, 남긴곳)}`))}`);
430
+ 말하기('');
431
+ return 통과수 === 판수 ? 0 : 1;
432
+ }
@@ -0,0 +1,95 @@
1
+ // `deel eval --init` 이 만들어 주는 예제 과제 다섯 (src/eval/run.js).
2
+ //
3
+ // ── 왜 파일이 아니라 글로 들고 있나 ─────────────────────────────────────
4
+ //
5
+ // npm 으로 받는 사람에게는 저장소의 폴더가 안 온다(package.json 의 files). 과제를
6
+ // 폴더로 두면 `--init` 이 복사할 원본이 설치본에 없다. 그래서 여기 글로 든다.
7
+ //
8
+ // ── 무엇을 재나 ──────────────────────────────────────────────────────────
9
+ //
10
+ // 코딩 에이전트가 실제로 자주 받는 일 다섯 가지를 하나씩 담았다. 크기는 작게 —
11
+ // 한 과제가 사내 모델로 1~2분 안에 끝나야 판마다 돌려 볼 마음이 난다.
12
+ //
13
+ // 01 버그 고치기 원인을 찾아 한 줄 고치기
14
+ // 02 기능 더하기 적힌 규칙대로 함수 하나를 새로 쓰기
15
+ // 03 여러 파일 이름 바꾸기 부르는 곳까지 빠짐없이
16
+ // 04 빨간 검사 고치기 검사를 건드리지 않고 코드를 고치기 — 완료 검사 고리(doneCheck)를 탄다
17
+ // 05 CRLF 파일 고치기 윈도 줄끝을 지키며 한 줄만 바꾸기
18
+ //
19
+ // 정답 검사(golden/)는 **모델이 일을 끝낸 뒤에** 작업 폴더에 들어간다. 모델은 못 본다.
20
+ // 그래서 「검사를 통과하게 검사를 고치는」 길이 없다 — 이게 정적 검사와 다른 점이다.
21
+
22
+ export const 예제과제 = {
23
+ '01-버그고치기': {
24
+ task: {
25
+ prompt: 'src/sum.js 의 sum() 이 목록의 합을 틀리게 돌려줍니다. 원인을 찾아 고쳐 주세요.',
26
+ check: 'node golden-check.mjs',
27
+ },
28
+ start: {
29
+ 'package.json': '{ "type": "module" }\n',
30
+ 'src/sum.js': '// 목록의 합을 돌려준다.\nexport function sum(list) {\n let total = 0;\n for (let i = 0; i < list.length - 1; i++) total += list[i];\n return total;\n}\n',
31
+ },
32
+ golden: {
33
+ 'golden-check.mjs': "import assert from 'node:assert/strict';\nimport { sum } from './src/sum.js';\n\nassert.equal(sum([1, 2, 3]), 6);\nassert.equal(sum([]), 0);\nassert.equal(sum([5]), 5);\nassert.equal(sum([-1, 1, 10]), 10);\nconsole.log('ok');\n",
34
+ },
35
+ },
36
+ '02-기능더하기': {
37
+ task: {
38
+ prompt: 'src/text.js 에 slugify(text) 를 더해 주세요. 영문은 소문자로 바꾸고, 영문 글자와 숫자가 아닌 글자가 이어진 자리는 하이픈(-) 하나로 바꾸고, 앞뒤 하이픈은 떼어 냅니다. 예: "Hello, World!" → "hello-world", " A B " → "a-b". 이미 있는 capitalize 는 그대로 둡니다.',
39
+ check: 'node golden-check.mjs',
40
+ },
41
+ start: {
42
+ 'package.json': '{ "type": "module" }\n',
43
+ 'src/text.js': "// 글 다루는 작은 함수들.\nexport function capitalize(s) {\n return s.charAt(0).toUpperCase() + s.slice(1);\n}\n",
44
+ },
45
+ golden: {
46
+ 'golden-check.mjs': "import assert from 'node:assert/strict';\nimport { slugify, capitalize } from './src/text.js';\n\nassert.equal(slugify('Hello, World!'), 'hello-world');\nassert.equal(slugify(' A B '), 'a-b');\nassert.equal(slugify('Version 2.1.0'), 'version-2-1-0');\nassert.equal(slugify('---'), '');\nassert.equal(slugify('abc'), 'abc');\nassert.equal(capitalize('deel'), 'Deel');\nconsole.log('ok');\n",
47
+ },
48
+ },
49
+ '03-이름바꾸기': {
50
+ task: {
51
+ prompt: 'getUser 함수의 이름을 fetchUser 로 바꿔 주세요. 부르는 곳도 모두 바꾸고, 동작은 그대로 둡니다.',
52
+ check: 'node golden-check.mjs',
53
+ },
54
+ start: {
55
+ 'package.json': '{ "type": "module" }\n',
56
+ 'src/user.js': "export function getUser(id) {\n return { id, name: 'user' + id };\n}\n",
57
+ 'src/profile.js': "import { getUser } from './user.js';\n\nexport function profileName(id) {\n return getUser(id).name;\n}\n",
58
+ 'src/admin.js': "import { getUser } from './user.js';\n\nexport function isAdmin(id) {\n return getUser(id).id === 0;\n}\n",
59
+ },
60
+ golden: {
61
+ 'golden-check.mjs': "import assert from 'node:assert/strict';\nimport { readdirSync, readFileSync } from 'node:fs';\nimport { fetchUser } from './src/user.js';\nimport { profileName } from './src/profile.js';\nimport { isAdmin } from './src/admin.js';\n\nassert.deepEqual(fetchUser(3), { id: 3, name: 'user3' });\nassert.equal(profileName(3), 'user3');\nassert.equal(isAdmin(0), true);\nassert.equal(isAdmin(1), false);\nconst 남은 = readdirSync('src').filter((f) => readFileSync('src/' + f, 'utf8').includes('getUser'));\nassert.deepEqual(남은, [], '옛 이름이 남은 파일: ' + 남은.join(', '));\nconsole.log('ok');\n",
62
+ },
63
+ },
64
+ '04-빨간검사고치기': {
65
+ task: {
66
+ prompt: 'npm test 가 실패합니다. 코드를 고쳐서 통과하게 해 주세요. 검사 파일(test.mjs)은 고치지 마세요.',
67
+ // 한 명령으로 둔다 — `&&` 는 PowerShell 5.1 을 셸로 고른 PC 에서 안 먹는다(tools/shell.js).
68
+ check: 'node golden-check.mjs',
69
+ // 모델이 끝내려 할 때 deel 이 이걸 돌린다 (agent/donecheck.js). 실패하면 이어서 고치게 한다.
70
+ doneCheck: 'npm test',
71
+ },
72
+ start: {
73
+ 'package.json': '{ "type": "module", "scripts": { "test": "node test.mjs" } }\n',
74
+ 'src/date.js': '// 날짜를 YYYY-MM-DD 로 적는다.\nexport function ymd(d) {\n return `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;\n}\n',
75
+ 'test.mjs': "import assert from 'node:assert/strict';\nimport { ymd } from './src/date.js';\n\nassert.equal(ymd(new Date(2024, 0, 5)), '2024-01-05');\nconsole.log('ok');\n",
76
+ },
77
+ golden: {
78
+ // 모델이 test.mjs 를 고쳤어도 원래 것으로 되돌려 놓고 돌린다.
79
+ 'test.mjs': "import assert from 'node:assert/strict';\nimport { ymd } from './src/date.js';\n\nassert.equal(ymd(new Date(2024, 0, 5)), '2024-01-05');\nconsole.log('ok');\n",
80
+ 'golden-check.mjs': "import assert from 'node:assert/strict';\nimport { ymd } from './src/date.js';\n\nassert.equal(ymd(new Date(2024, 0, 5)), '2024-01-05');\nassert.equal(ymd(new Date(2024, 11, 31)), '2024-12-31');\nassert.equal(ymd(new Date(2025, 9, 10)), '2025-10-10');\nassert.equal(ymd(new Date(1999, 8, 9)), '1999-09-09');\nconsole.log('ok');\n",
81
+ },
82
+ },
83
+ '05-CRLF파일': {
84
+ task: {
85
+ prompt: 'config.ini 의 port 를 9090 으로 바꿔 주세요. 다른 줄은 건드리지 마세요.',
86
+ check: 'node golden-check.mjs',
87
+ },
88
+ start: {
89
+ 'config.ini': '[server]\r\nport=8080\r\nhost=localhost\r\n',
90
+ },
91
+ golden: {
92
+ 'golden-check.mjs': "import assert from 'node:assert/strict';\nimport { readFileSync } from 'node:fs';\n\nassert.equal(readFileSync('config.ini', 'utf8'), '[server]\\r\\nport=9090\\r\\nhost=localhost\\r\\n', '줄끝(CRLF)을 지키며 port 한 줄만 바뀌어야 합니다');\nconsole.log('ok');\n",
93
+ },
94
+ },
95
+ };
package/src/i18n/en.js CHANGED
@@ -323,6 +323,10 @@ export const en = {
323
323
  'approve.strict.name': 'ask always',
324
324
  'approve.strict.short': 'always',
325
325
  'approve.strict.line': 'asks before every file edit and every command',
326
+ 'approve.newFile': 'new file',
327
+ 'approve.locked': 'A managed policy keeps approval at {바닥} or stricter — {곳}',
328
+ 'approve.lockedUnknown': 'The managed policy\'s approval value "{값}" is unknown, so it counts as strict, the narrowest — {곳}',
329
+ 'approve.noAlways': 'A managed policy sets approval, so "don\'t ask again" is not offered',
326
330
  'work.title': 'Work modes',
327
331
  'work.canEdit': 'edits files',
328
332
  'work.readOnly': 'read-only',
@@ -388,6 +392,8 @@ export const en = {
388
392
  'plan.ask': 'Go ahead with this?',
389
393
  'plan.hint': '⏎ go · y with a note · 3 a note on that step · n cancel',
390
394
  'plan.cancelled': 'Stopped. The plan is still above — just say what you want next.',
395
+ 'plan.timedOut': 'No answer in {초}s — going ahead with the plan.',
396
+ 'plan.autoGo': 'Auto mode — going ahead with the plan without asking.',
391
397
  'plan.going': 'Going ahead with the plan.',
392
398
  'plan.goingWith': 'Going ahead, with your note — {말}',
393
399
  'plan.replan': 'Redoing the plan with that in mind.',
@@ -398,6 +404,9 @@ export const en = {
398
404
  'ask.pickPrompt': '1-{끝} (a note may follow), or type your own',
399
405
  'ask.freePrompt': 'Answer',
400
406
  'ask.stopped': 'Stopped — treating it as unanswered.',
407
+ 'ask.autoWait': 'If no answer comes in {초}s, it carries on by itself',
408
+ 'ask.timedOut': 'No answer in {초}s — carrying on by itself.',
409
+ 'ask.autoSkip': 'Auto mode, so it carries on without asking — {물음}',
401
410
 
402
411
  // ── When the server pushes back (backend/retry.js) ────────────────────
403
412
  'loop.backoff': 'The server pushed back ({무엇}) — calling again in {초}s ({n}/{max})',
@@ -508,6 +517,17 @@ export const en = {
508
517
  'ev.hookNote': 'hook: {말}',
509
518
  'ev.hookBlock': 'A hook blocked this',
510
519
  'ev.nudgeRead': 'It was about to finish having only read things — pushed back once',
520
+ 'check.start': 'Completion check ({판}/{최대}) — {명령}',
521
+ 'check.pass': 'Completion check passed — {명령}',
522
+ 'check.fail': 'Completion check failed ({판}/{최대}) — {요약}',
523
+ 'check.retry': 'Sending the failure back to fix',
524
+ 'check.gaveUp': 'Still failing after {판} runs — ending as failed',
525
+ 'check.skipped': 'Could not run the completion check — {까닭}',
526
+ 'cli.checkFlag': 'run this check before finishing; failures go back to be fixed, and still failing ends with 8',
527
+ 'cli.evalTitle': 'Golden set',
528
+ 'cli.evalWhy': '— run a set of tasks on the real model and measure how many it gets done',
529
+ 'cli.evalInit': 'create five example tasks in golden/',
530
+ 'cli.evalRun': 'run deel run on each task in a temp folder, then grade it with checks it never saw',
511
531
  'ev.learnedCtx': 'Setting the context limit to {한계} as the server reported, and calling again',
512
532
  'ev.learnedOut': 'Setting the answer-length limit to {한계} as the server reported, and calling again',
513
533
  'ev.reset': 'Out of room — cleared {버린수} earlier messages and carrying on, {할일} to-do items left',
package/src/i18n/ja.js CHANGED
@@ -290,6 +290,10 @@ export const ja = {
290
290
  'approve.strict.name': '毎回訊く',
291
291
  'approve.strict.short': '毎回',
292
292
  'approve.strict.line': 'ファイルを直すたび、コマンドを打つたびに訊きます',
293
+ 'approve.newFile': '新しいファイル',
294
+ 'approve.locked': '管理ポリシーが承認を {바닥} より下げられないようにしています — {곳}',
295
+ 'approve.lockedUnknown': '管理ポリシーの approval の値「{값}」が分からないため、いちばん狭い strict とみなします — {곳}',
296
+ 'approve.noAlways': '管理ポリシーが承認を固定しているため「今後は訊かない」はありません',
293
297
  'work.title': '作業モード',
294
298
  'work.canEdit': 'ファイルを変更',
295
299
  'work.readOnly': '読むだけ',
@@ -353,6 +357,8 @@ export const ja = {
353
357
  'plan.ask': 'この通り進めますか?',
354
358
  'plan.hint': '⏎ 進む · y 一言添えて進む · 3 その段階に一言 · n やめる',
355
359
  'plan.cancelled': 'やめました。計画は上に残っているので、続きから言ってください。',
360
+ 'plan.timedOut': '{초}秒答えがなかったので、計画どおり進めます。',
361
+ 'plan.autoGo': '自律モードなので、聞かずに計画どおり進めます。',
356
362
  'plan.going': '計画どおり進めます。',
357
363
  'plan.goingWith': '一言を添えて進めます — {말}',
358
364
  'plan.replan': 'その点を踏まえて計画を出し直します。',
@@ -363,6 +369,9 @@ export const ja = {
363
369
  'ask.pickPrompt': '1-{끝}(後ろに一言添えても可)、または自分で書く',
364
370
  'ask.freePrompt': '答え',
365
371
  'ask.stopped': '止めました — 答えなかったものとして進みます。',
372
+ 'ask.autoWait': '{초}秒以内に答えがなければ自分で進めます',
373
+ 'ask.timedOut': '{초}秒答えがなかったので、自分で進めます。',
374
+ 'ask.autoSkip': '自律モードなので聞かずに進めます — {물음}',
366
375
  'loop.backoff': 'サーバーに押し返されました ({무엇}) — {초} 秒後にもう一度 ({n}/{max})',
367
376
  'cost.retries': '再試行',
368
377
  // ── 数える言葉 ────────────────────────────────────────────────────────
@@ -466,6 +475,17 @@ export const ja = {
466
475
  'ev.hookNote': 'フック: {말}',
467
476
  'ev.hookBlock': 'フックが止めました',
468
477
  'ev.nudgeRead': '読むだけで終えようとしたので、一度押し戻しました',
478
+ 'check.start': '完了チェック ({판}/{최대}) — {명령}',
479
+ 'check.pass': '完了チェック合格 — {명령}',
480
+ 'check.fail': '完了チェック失敗 ({판}/{최대}) — {요약}',
481
+ 'check.retry': '失敗内容を返して直させます',
482
+ 'check.gaveUp': '{판}回回しても失敗しました — 失敗として終えます',
483
+ 'check.skipped': '完了チェックを回せませんでした — {까닭}',
484
+ 'cli.checkFlag': '終える前にこのチェックを回し、通れば終了(失敗なら直させ、最後まで失敗なら 8)',
485
+ 'cli.evalTitle': 'ゴールデンセット',
486
+ 'cli.evalWhy': '— 課題集を実際のモデルで回し、いくつこなせるかを測ります',
487
+ 'cli.evalInit': 'golden/ に例題を五つ作ります',
488
+ 'cli.evalRun': '課題ごとに一時フォルダで deel run を回し、終わった後に正解チェックで採点します',
469
489
  'ev.learnedCtx': 'サーバーが教えてくれたコンテキスト上限 {한계} に合わせて呼び直します',
470
490
  'ev.learnedOut': 'サーバーが教えてくれた答えの長さ上限 {한계} に合わせて呼び直します',
471
491
  'ev.reset': '場所が足りず、前の会話 {버린수} 件を消して続けます — 残りのやること {할일} 件',