deel-local-cli 1.0.2 → 1.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.
@@ -13,7 +13,7 @@
13
13
  // 읽기만 하는 도구. 무엇을 바꾸지 않는다.
14
14
  // Recall 은 지난 대화를 찾는다 — 파일은 안 건드리므로 읽기 쪽이다.
15
15
  // 묻기 모드에도 준다: "저번에 이거 어떻게 했더라" 가 딱 묻기 모드의 일이다.
16
- const 읽기 = ['Read', 'Glob', 'Grep', 'WebFetch', 'Skill', 'Recall'];
16
+ const 읽기 = ['Read', 'Outline', 'Glob', 'Grep', 'WebFetch', 'Skill', 'Recall'];
17
17
  // 계획을 적는 도구. 파일을 안 건드리므로 읽기 전용 모드에서도 준다.
18
18
  //
19
19
  // Remember 도 여기 있다. 기억은 사용자의 소스를 안 건드리고 .deel/memory.md
@@ -23,7 +23,25 @@ const 계획 = ['TodoWrite', 'Remember'];
23
23
  // 바꾸는 도구.
24
24
  // Append 는 Write 와 짝이다 — 출력 상한이 작은 모델이 큰 파일을 나눠 쓰는 길이다.
25
25
  // Write 를 주는 자리에는 반드시 같이 준다. 하나만 주면 나눠 쓸 방법이 없어진다.
26
- const 쓰기 = ['Write', 'Append', 'Edit', 'Bash'];
26
+ // Jobs Bash 짝이다. Bash 를 주는 자리에는 반드시 같이 준다 —
27
+ // background 로 띄워 놓고 읽을 길이 없으면 띄운 것이 그냥 유령이 된다.
28
+ const 쓰기 = ['Write', 'Append', 'Edit', 'Bash', 'Jobs'];
29
+ // 만든 것을 확인하는 도구. 아무것도 안 바꾸지만 **쓰는 모드에만** 준다 —
30
+ // 안 만든 모드에서 확인할 것이 없고, 도구 정의로 나가는 자리만 먹는다.
31
+ const 확인 = ['Verify'];
32
+ /*
33
+ * 일을 쪼개는 도구.
34
+ *
35
+ * 파일을 바꾸는 모드에만 준다. 읽기만 하는 모드(설계·계획·묻기)는 한 창 안에서
36
+ * 답을 내는 것이 일이고, 거기에 하위 작업을 얹으면 얻는 것보다 도구 정의로
37
+ * 나가는 자리가 더 아깝다.
38
+ *
39
+ * 안전 쪽으로도 그렇다. 하위 작업은 제 모드를 스스로 고르는데, 읽기 전용
40
+ * 모드에 이걸 쥐여 주면 "파일을 안 바꾼다" 는 약속을 하위가 깨고 나갈 길이
41
+ * 하나 생긴다. 그 길은 loop.js 와 task.js 에서 두 겹으로 막아 두었지만,
42
+ * 애초에 안 주는 것이 제일 확실하다.
43
+ */
44
+ const 쪼개기 = ['Task'];
27
45
 
28
46
  export const MODES = {
29
47
  // 처음에는 여기서 시작한다.
@@ -40,19 +58,34 @@ export const MODES = {
40
58
  en: 'Auto',
41
59
  glyph: '◎',
42
60
  hint: '요청에 따라 알맞은 모드로',
43
- tools: [...읽기, ...계획, ...쓰기],
61
+ tools: [...읽기, ...계획, ...쓰기, ...확인, ...쪼개기],
44
62
  effort: 'save',
45
63
  think: null,
46
- steps: 24,
64
+ // 작은 창용 짧은 판. 빠진 규칙은 없고 설득하는 문장만 없다 — 아래 말() 참고.
65
+ say짧게: [
66
+ '**종합** 모드다. 무슨 일이 올지 정해져 있지 않다.',
67
+ '- 무슨 일인지 먼저 가늠하고 그에 맞게 해라.',
68
+ '- 큰 일이면 TodoWrite 로 쪼개 적고 **다 끝낸다.** 덩이가 여럿이면 Task 로 떼어 준다.',
69
+ '- 남의 코드는 Outline 으로 모양부터. 통째로 Read 하지 마라.',
70
+ '- 파일 여러 개는 Write 한 번에 (files 배열), 고칠 자리 여럿은 Edit 한 번에 (edits 배열).',
71
+ '- 끝내지 않는 명령(dev 서버·watch)은 Bash 에 background: true. Jobs 로 읽고 끝낸다.',
72
+ '- 끝내기 전에 Verify. 확인 못 한 것을 됐다고 하지 마라.',
73
+ ].join('\n'),
47
74
  say: [
48
75
  '지금은 **종합** 모드다. 무슨 일이 올지 정해져 있지 않다.',
49
76
  '',
50
77
  '- 시키는 일이 무엇인지 먼저 가늠하고, 그에 맞는 방식으로 해라.',
51
78
  ' 고치는 일이면 읽고 나서 고치고, 원인을 찾는 일이면 확인부터 하고,',
52
79
  ' 설명하는 일이면 근거를 파일에서 대라.',
53
- '- 큰 일이면 TodoWrite 로 쪼개 적고 하나씩 해라.',
54
- '- 확인할 있는 것은 확인해라. 확인 것을 됐다고 하지 마라.',
55
- '- 시키지 않은 일을 벌이지 마라.',
80
+ '- 큰 일이면 TodoWrite 로 쪼개 적고 **적은 것을 다 끝낸다.** 적어 놓고 묻지 마라.',
81
+ ' 덩이가 여럿이면 Task 떼어 준다 하위는 창에서 돌아 창이 안 찬다.',
82
+ '- 남의 코드는 Outline 으로 모양부터 본다. 통째로 Read 하지 마라.',
83
+ '- 파일을 여러 개 만들 때는 Write 한 번에 (files 배열). 한 개씩 부르지 마라.',
84
+ ' 고칠 자리가 여러 군데일 때도 마찬가지로 Edit 한 번에 (edits 배열).',
85
+ '- 끝나지 않는 명령(dev 서버·watch)은 Bash 에 background: true 를 준다. 그냥 부르면',
86
+ ' 시간 초과로 죽는다. 띄운 뒤에는 Jobs 로 출력을 읽고, 일이 끝나면 Jobs 로 끝낸다.',
87
+ '- 확인할 수 있는 것은 확인해라 — Verify 를 부른다. 확인 못 한 것을 됐다고 하지 마라.',
88
+ '- 시킨 일을 해내는 데 필요한 것은 한다. 그것과 상관없는 일을 벌이지 마라.',
56
89
  ].join('\n'),
57
90
  },
58
91
 
@@ -62,20 +95,43 @@ export const MODES = {
62
95
  en: 'Code',
63
96
  glyph: '◆',
64
97
  hint: '고치고 만든다',
65
- tools: [...읽기, ...계획, ...쓰기],
98
+ tools: [...읽기, ...계획, ...쓰기, ...확인, ...쪼개기],
66
99
  effort: 'save', // 첫 판단만 세게, 이어가기는 얕게
67
100
  think: null, // 사용자가 정한 값을 그대로 쓴다
68
- steps: 24,
101
+
102
+ say짧게: [
103
+ '**구현**이다. 이 순서를 지켜라.',
104
+ '1. 남의 코드면 Outline 으로 모양부터. 고칠 자리를 고른 뒤 **그 파일만** Read.',
105
+ '2. 고칠 파일은 반드시 먼저 Read.',
106
+ '3. 주변 코드의 관례를 따른다. 새 관례를 들여오지 마라.',
107
+ '4. 파일 여럿은 Write 한 번에 (files 배열). 고칠 자리 여럿은 Edit 한 번에 (edits 배열).',
108
+ '5. 갈래가 여럿이면 Task 로 떼어 준다.',
109
+ '6. **끝내기 전에 Verify.** 탈이 나오면 고치고 다시 부른다.',
110
+ ' 띄워 봐야 하면 Bash 에 background: true — 그냥 부르면 시간 초과로 죽는다. Jobs 로 읽고 Jobs 로 끝낸다.',
111
+ '7. 무엇을 왜 바꿨는지 한두 줄로. 코드를 붙여넣지 마라.',
112
+ ].join('\n'),
113
+
69
114
  say: [
70
115
  "지금 하는 일은 **구현**이다. 아래 순서를 지켜라.",
71
116
  "",
72
- "1. 고칠 파일을 먼저 Read 읽는다. 읽고 고치려 하면 도구가 거절한다.",
73
- "2. 주변 코드의 관례를 따른다 이름 짓는 법, 오류 다루는 법, 주석 밀도까지.",
117
+ "1. 남의 코드를 만지는 일이면 Outline 으로 **모양부터** 본다. 파일을 통째로",
118
+ " Read 하지 마라폴더 하나가 Outline 으로는 몇십 분의 일이다.",
119
+ " 거기서 고칠 자리를 고른 다음, **그 파일만** Read 한다.",
120
+ "2. 고칠 파일은 반드시 먼저 Read 한다. 안 읽고 고치려 하면 도구가 거절한다.",
121
+ "3. 주변 코드의 관례를 따른다 — 이름 짓는 법, 오류 다루는 법, 주석 밀도까지.",
74
122
  " 새 관례를 들여오지 마라. 이 코드가 이미 하고 있는 대로 한다.",
75
- "3. 번에 하나씩 고친다. 여러 파일에 흩뿌리지 마라.",
76
- "4. 고친 뒤에는 확인할 방법이 있으면 실제로 돌려 본다 (검사·빌드·실행).",
77
- " 확인 했으면 \"확인 했다\" 말한다. 됐다고 하지 마라.",
78
- "5. 끝나면 무엇을 바꿨는지 한두 줄로 말한다. 코드를 통째로 붙여넣지 마라.",
123
+ "4. 필요한 파일을 만들고 다 고친다. 파일만 건드려 놓고 멈추지 마라.",
124
+ " 새로 만드는 일이면 폴더 구조부터 잡고, **Write 번에 여러 파일**을 만든다",
125
+ " (files 배열). 고칠 자리가 여러 군데면 **Edit 번에** 보낸다 (edits 배열).",
126
+ " 번에 하나씩 부르면 수만큼 왕복이 늘어 분이 그냥 간다.",
127
+ "5. 파일을 여러 갈래로 나눠 만들어야 하면 Task 로 덩이를 떼어 준다.",
128
+ " 하위 작업은 제 창에서 돌고 너에게는 요약만 온다 — 네 창이 안 찬다.",
129
+ "6. **끝내기 전에 Verify 를 부른다.** 파일이 있다는 것과 되는 것은 다르다.",
130
+ " 탈이 나오면 고치고 다시 부른다. 확인 못 한 것은 \"확인 못 했다\" 고 말한다.",
131
+ " 실제로 띄워 봐야 아는 일이면 (dev 서버·watch) **Bash 에 background: true** 를 준다.",
132
+ " 그냥 부르면 끝나지 않아서 시간 초과로 죽는다. 띄운 뒤 Jobs 로 출력을 읽고,",
133
+ " 일이 끝나면 Jobs 로 반드시 끝낸다 — 안 끝내면 그 서버가 계속 포트를 문다.",
134
+ "7. 끝나면 무엇을 왜 바꿨는지 한두 줄로 말한다. 코드를 통째로 붙여넣지 마라.",
79
135
  ].join('\n'),
80
136
  },
81
137
 
@@ -88,12 +144,12 @@ export const MODES = {
88
144
  tools: [...읽기, ...계획],
89
145
  effort: 'deep',
90
146
  think: 'high',
91
- steps: 20,
92
147
  say: [
93
148
  "지금 하는 일은 **설계**다. 파일을 바꾸는 도구는 주어지지 않았다.",
94
149
  "",
95
150
  "먼저 읽어라. 지금 구조를 모르면 설계가 아니라 상상이다.",
96
- " - 관련된 파일을 Glob/Grep 으로 찾고 Read 로 실제로 읽는다",
151
+ " - Outline 으로 폴더 모양부터 본다. 그 다음 Glob/Grep 으로 좁히고,",
152
+ " 꼭 필요한 파일만 Read 로 실제로 읽는다",
97
153
  " - 무엇이 무엇에 기대고 있는지(의존 방향)를 파악한다",
98
154
  "",
99
155
  "그 다음 이 차례로 답한다.",
@@ -117,7 +173,6 @@ export const MODES = {
117
173
  tools: [...읽기],
118
174
  effort: 'even',
119
175
  think: 'low',
120
- steps: 8,
121
176
  say: [
122
177
  "지금 하는 일은 **설명**이다. 아무것도 바꾸지 않는다.",
123
178
  "",
@@ -134,10 +189,9 @@ export const MODES = {
134
189
  en: 'Debug',
135
190
  glyph: '◉',
136
191
  hint: '원인을 찾는다',
137
- tools: [...읽기, ...계획, ...쓰기],
192
+ tools: [...읽기, ...계획, ...쓰기, ...확인, ...쪼개기],
138
193
  effort: 'deep',
139
194
  think: 'high',
140
- steps: 32, // 원인 찾기는 왔다 갔다 하므로 여유를 준다
141
195
  say: [
142
196
  "지금 하는 일은 **원인 찾기**다. 짐작으로 고치지 마라.",
143
197
  "",
@@ -163,13 +217,12 @@ export const MODES = {
163
217
  tools: [...읽기, ...계획],
164
218
  effort: 'deep',
165
219
  think: 'high',
166
- steps: 16,
167
220
  say: [
168
221
  "지금 하는 일은 **계획 세우기**다. 파일을 바꾸는 도구는 주어지지 않았다.",
169
222
  "코드를 고치려 들지 마라. 계획을 내고 멈춘다.",
170
223
  "",
171
224
  "먼저 확인하라 — 지금 무엇이 어떻게 되어 있는지 모르면 계획이 아니라 희망이다.",
172
- " Glob/Grep 으로 관련 파일을 찾고, Read 실제로 읽어라.",
225
+ " Outline 으로 모양부터 보고, Glob/Grep 으로 좁힌 뒤, 필요한 것만 Read 해라.",
173
226
  "",
174
227
  "그 다음 이 차례로 적어라.",
175
228
  " 1. 목표 — 무엇이 끝나면 다 된 것인가 (확인할 수 있는 문장으로)",
@@ -192,19 +245,32 @@ export const MODES = {
192
245
  en: 'Orchestrator',
193
246
  glyph: '❋',
194
247
  hint: '큰 일을 쪼개서 끝까지',
195
- tools: [...읽기, ...계획, ...쓰기],
248
+ tools: [...읽기, ...계획, ...쓰기, ...확인, ...쪼개기],
196
249
  effort: 'save',
197
250
  think: null,
198
- steps: 40, // 여러 갈래를 끝까지 끌고 가야 한다
251
+ say짧게: [
252
+ '**큰 일을 끝까지 끌고 가기**다.',
253
+ '1. 시작하자마자 TodoWrite 로 전체를 단계로 쪼개 적어라.',
254
+ '2. **단계마다 Task 로 떼어 줘라.** 직접 다 하면 네 창이 차서 하던 일을 잊는다.',
255
+ ' 하위는 이 대화를 못 본다 — 배경·정한 것·파일 경로를 다 적어 줘라.',
256
+ '3. 한 번에 하나만 진행 중으로. 끝나면 바로 표시하고 다음으로.',
257
+ '4. 단계 끝마다 Verify.',
258
+ '5. 막히면 멈추고 보고해라. 우회로를 몰래 타지 마라.',
259
+ '다 끝나면 한 것과 못 한 것을 같이 요약해라. 못 한 것을 빼지 마라.',
260
+ ].join('\n'),
199
261
  say: [
200
262
  "지금 하는 일은 **큰 일을 끝까지 끌고 가기**다.",
201
263
  "",
202
264
  " 1. 시작하자마자 TodoWrite 로 전체를 단계로 쪼개 적어라. 머릿속에만 두지 마라.",
203
265
  " 단계는 각각 따로 확인할 수 있는 크기여야 한다.",
204
- " 2. 번에 하나만 진행 중으로 둬라. 끝나면 바로 표시하고 다음으로 넘어가라.",
205
- " 3. 단계 끝에서 확인하라. 확인 없이 넘어가면 어디서 어긋났는지 찾는다.",
206
- " 4. 막히면 멈추고 무엇에 막혔는지 보고하라. 우회로를 몰래 타지 마라.",
207
- " 5. 계획이 틀린 것을 알게 되면 목록을 고쳐라. 틀린 계획을 끝까지 밀지 마라.",
266
+ " 2. **단계 하나하나를 Task 떼어 줘라.** 이게 모드의 핵심이다 ",
267
+ " 네가 직접 하면 파일 내용이 전부 창에 쌓여서, 서너 단계째에",
268
+ " 앞엣말이 접혀 나가고 무엇을 하던 중이었는지 잊는다.",
269
+ " 하위에게는 배경·정한 것·파일 경로를 적어 줘라. 하위는 대화를 본다.",
270
+ " 3. 한 번에 하나만 진행 중으로 둬라. 끝나면 바로 표시하고 다음으로 넘어가라.",
271
+ " 4. 각 단계 끝에서 Verify 로 확인하라. 확인 없이 넘어가면 어디서 어긋났는지 못 찾는다.",
272
+ " 5. 막히면 멈추고 무엇에 막혔는지 보고하라. 우회로를 몰래 타지 마라.",
273
+ " 6. 계획이 틀린 것을 알게 되면 목록을 고쳐라. 틀린 계획을 끝까지 밀지 마라.",
208
274
  "",
209
275
  "다 끝나면 무엇을 했는지, 무엇을 못 했는지 같이 요약하라.",
210
276
  "못 한 것을 빼고 요약하지 마라.",
@@ -236,6 +302,21 @@ export function get(id) {
236
302
  return MODES[normalize(id) ?? DEFAULT];
237
303
  }
238
304
 
305
+ /**
306
+ * 이 창 크기에 맞는 모드 설명.
307
+ *
308
+ * 24k 아래에서는 짧은 판을 쓴다 — 도구 설명(budget.js 의 설명길이)과 기본
309
+ * 규칙(session.js)이 줄어드는 자리와 같은 경계다. 셋이 같이 움직여야
310
+ * '작은 창에서는 고정 몫을 줄인다' 가 흩어진 세 결정이 아니라 한 결정이 된다.
311
+ *
312
+ * 짧은 판이 없는 모드는 원래 짧은 것들이다(묻기 125토큰). 그냥 그대로 쓴다.
313
+ */
314
+ export function 말(id, ctx) {
315
+ const m = get(id);
316
+ const 좁은가 = Number(ctx) > 0 && Number(ctx) < 24000;
317
+ return (좁은가 && m.say짧게) ? m.say짧게 : m.say;
318
+ }
319
+
239
320
  /** Ctrl+O 로 돌릴 때 다음 모드. (Shift+Tab 은 승인 방식이 가져갔다) */
240
321
  export function next(id) {
241
322
  const i = ORDER.indexOf(normalize(id) ?? DEFAULT);
@@ -0,0 +1,171 @@
1
+ /*
2
+ * 이 폴더가 무슨 프로젝트인가 — 켤 때 한 번 읽어 프롬프트에 넣는다.
3
+ *
4
+ * 왜 필요한가:
5
+ * 남의 코드가 있는 폴더에서 켜면 모델은 아무것도 모르는 채로 시작했다.
6
+ * 그래서 매번 같은 세 걸음을 다시 밟는다 — Glob 으로 위쪽을 훑고,
7
+ * package.json 을 읽고, 검사를 어떻게 돌리는지 찾는다. 로컬 모델은 한 걸음이
8
+ * 20~40초라 **일을 시작하기도 전에 2분**이 간다. 그 세 걸음의 답은 켤 때
9
+ * 이미 다 알 수 있는 것이다.
10
+ *
11
+ * 더 나쁜 쪽도 있다. 모델이 그 세 걸음을 **안 밟고** 그냥 시작하는 경우다.
12
+ * 그러면 이 프로젝트가 이미 쓰는 것을 모른 채 제 관례로 파일을 만든다 —
13
+ * npm 프로젝트에 requirements.txt 를 만들어 놓는 식이다.
14
+ *
15
+ * 무엇을 넣나 (값이 큰 순서):
16
+ * 1. 돌릴 수 있는 명령 — npm scripts 가 곧 '이 프로젝트에서 되는 일' 이다
17
+ * 2. 위쪽 생김새 — Glob 한 번을 아낀다
18
+ * 3. 무슨 갈래인가 — node·python·go·rust·java·c#
19
+ * 4. 지금 git 가지 — 어디에 커밋하게 되는지
20
+ *
21
+ * 무엇을 안 하나:
22
+ * git 을 **띄우지 않는다.** .git/HEAD 를 그냥 읽는다. 켤 때 자식 프로세스를
23
+ * 부르면 큰 저장소에서 몇 초가 걸리고, 그 몇 초는 화면이 멈춘 채로 간다.
24
+ * 안 올린 변경 개수 같은 것은 모델이 필요하면 제 손으로 git 을 부르면 된다.
25
+ *
26
+ * 폴더를 훑지도 않는다. 위쪽 한 겹만 읽는다. 하위까지 내려가면 큰 저장소에서
27
+ * 느려지고, 어차피 프롬프트에 넣을 양은 한 줄뿐이다.
28
+ *
29
+ * 이 글도 **고정 몫**이다 — 매 요청에 통째로 나간다. 그래서 창 크기에 맞춘다.
30
+ * 8k 에서는 두 줄, 큰 창에서는 네 줄. 늘리면 test/compact.test.js 가 먼저 빨개진다.
31
+ */
32
+ import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
33
+ import { join } from 'node:path';
34
+
35
+ // 위쪽에 있어도 사람에게 아무 말도 안 해 주는 것들. 적어 봐야 자리만 먹는다.
36
+ const 안적을것 = new Set([
37
+ 'node_modules', '.git', '.deel', '.claude', '.vscode', '.idea', '__pycache__',
38
+ '.venv', 'venv', 'dist', 'build', 'target', 'out', '.next', '.cache', '.pytest_cache',
39
+ 'coverage', '.DS_Store', 'Thumbs.db',
40
+ ]);
41
+
42
+ // 파일 하나로 갈래가 정해지는 것들. 위에 있는 것이 먼저 이긴다.
43
+ const 표식 = [
44
+ { 파일: 'package.json', 갈래: 'node' },
45
+ { 파일: 'pyproject.toml', 갈래: 'python' },
46
+ { 파일: 'requirements.txt', 갈래: 'python' },
47
+ { 파일: 'go.mod', 갈래: 'go' },
48
+ { 파일: 'Cargo.toml', 갈래: 'rust' },
49
+ { 파일: 'pom.xml', 갈래: 'java (maven)' },
50
+ { 파일: 'build.gradle', 갈래: 'java (gradle)' },
51
+ { 파일: 'build.gradle.kts', 갈래: 'kotlin (gradle)' },
52
+ { 파일: 'Gemfile', 갈래: 'ruby' },
53
+ { 파일: 'composer.json', 갈래: 'php' },
54
+ { 파일: 'CMakeLists.txt', 갈래: 'c/c++ (cmake)' },
55
+ ];
56
+
57
+ /** 위쪽 한 겹. 폴더는 뒤에 / 를 붙여 파일과 구별한다. */
58
+ function 위쪽(root, 상한) {
59
+ let 것들;
60
+ try { 것들 = readdirSync(root); } catch { return { 목록: [], 더: 0 }; }
61
+ const 폴더 = [];
62
+ const 파일 = [];
63
+ for (const n of 것들) {
64
+ if (안적을것.has(n) || n.startsWith('.')) continue;
65
+ try { (statSync(join(root, n)).isDirectory() ? 폴더 : 파일).push(n); }
66
+ catch { /* 읽는 중에 사라졌으면 없는 셈 */ }
67
+ }
68
+ // 폴더가 먼저다. 구조를 먼저 보여 주는 편이 훑을 때 빠르다.
69
+ const 다 = [...폴더.sort().map((n) => `${n}/`), ...파일.sort()];
70
+ return { 목록: 다.slice(0, 상한), 더: Math.max(0, 다.length - 상한) };
71
+ }
72
+
73
+ /**
74
+ * 돌릴 수 있는 명령.
75
+ *
76
+ * 이름을 지어내지 않는다 — 적힌 것만 그대로 옮긴다. 없는 명령을 알려 주면
77
+ * 모델이 그걸 부르고, 실패하고, 다시 찾느라 걸음을 더 쓴다.
78
+ */
79
+ function 명령들(root, 갈래, 상한) {
80
+ if (갈래 !== 'node') return [];
81
+ try {
82
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
83
+ const s = pkg?.scripts;
84
+ if (!s || typeof s !== 'object') return [];
85
+ // 사람이 실제로 부르는 것부터. 나머지는 알파벳순으로 채운다.
86
+ const 앞선것 = ['dev', 'start', 'test', 'build', 'lint', 'typecheck'];
87
+ const 있는것 = Object.keys(s);
88
+ const 골라 = [
89
+ ...앞선것.filter((n) => 있는것.includes(n)),
90
+ ...있는것.filter((n) => !앞선것.includes(n)).sort(),
91
+ ];
92
+ return 골라.slice(0, 상한).map((n) => (n === 'test' || n === 'start' ? `npm ${n}` : `npm run ${n}`));
93
+ } catch { return []; }
94
+ }
95
+
96
+ /** package.json 의 이름. 폴더 이름과 다를 때가 많아 따로 본다. */
97
+ function 이름(root, 갈래) {
98
+ if (갈래 !== 'node') return null;
99
+ try {
100
+ const n = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))?.name;
101
+ return typeof n === 'string' && n ? n : null;
102
+ } catch { return null; }
103
+ }
104
+
105
+ /**
106
+ * 지금 git 가지.
107
+ *
108
+ * git 을 안 띄우고 .git/HEAD 를 읽는다. 떨어져 나온 머리(detached HEAD)면
109
+ * 가지 이름 대신 커밋 앞자리가 들어 있는데, 그때는 '가지 없음' 이라고 말한다 —
110
+ * 커밋 해시를 가지 이름처럼 적으면 모델이 그 이름으로 checkout 을 시도한다.
111
+ */
112
+ export function git가지(root) {
113
+ try {
114
+ const head = readFileSync(join(root, '.git', 'HEAD'), 'utf8').trim();
115
+ const m = head.match(/^ref:\s*refs\/heads\/(.+)$/);
116
+ if (m) return m[1];
117
+ return head ? '가지에 안 붙어 있음 (detached)' : null;
118
+ } catch { return null; }
119
+ }
120
+
121
+ /**
122
+ * 이 폴더의 지문.
123
+ *
124
+ * @param 창 모델 컨텍스트 길이. 여기에 맞춰 몇 줄까지 적을지 정한다.
125
+ * @returns {string|null} 프롬프트에 넣을 토막. 적을 것이 없으면 null.
126
+ */
127
+ export function 지문(root, 창 = null) {
128
+ // 좁을수록 적게. 이건 접히지 않는 고정 몫이라, 창의 몇 %를 쓸지가 곧 손해다.
129
+ const n = Number(창) || 0;
130
+ const 넉넉한가 = n >= 32000;
131
+ const 아주좁은가 = n > 0 && n < 12000;
132
+ const 위쪽상한 = 아주좁은가 ? 10 : 넉넉한가 ? 24 : 16;
133
+ const 명령상한 = 아주좁은가 ? 4 : 넉넉한가 ? 8 : 6;
134
+
135
+ let 갈래 = null;
136
+ for (const t of 표식) {
137
+ if (existsSync(join(root, t.파일))) { 갈래 = t.갈래; break; }
138
+ }
139
+
140
+ const 줄들 = [];
141
+ const 이 = 이름(root, 갈래);
142
+ const 가지 = git가지(root);
143
+
144
+ const 머리 = [
145
+ 갈래 ? `${갈래} 프로젝트${이 ? ` (${이})` : ''}` : null,
146
+ 가지 ? `git ${가지}` : null,
147
+ ].filter(Boolean).join(' · ');
148
+ if (머리) 줄들.push(머리);
149
+
150
+ const 명 = 명령들(root, 갈래, 명령상한);
151
+ // 명령은 제일 값이 크다. 좁은 창에서 무엇 하나를 남긴다면 이것이다 —
152
+ // '이 프로젝트에서 검사를 어떻게 돌리나' 가 여기 다 들어 있다.
153
+ if (명.length) 줄들.push(`돌릴 수 있는 것: ${명.join(' · ')}`);
154
+
155
+ const w = 위쪽(root, 위쪽상한);
156
+ if (w.목록.length) {
157
+ 줄들.push(`위쪽: ${w.목록.join(' ')}${w.더 ? ` (그 밖에 ${w.더}개)` : ''}`);
158
+ }
159
+
160
+ if (!줄들.length) return null;
161
+ /*
162
+ * 마지막 한 줄이 중요하다.
163
+ *
164
+ * 이 토막만 보고 "다 알았다" 고 넘어가면 안 된다. 여기 적힌 것은 위쪽 한 겹과
165
+ * package.json 뿐이고, 하위 폴더 안은 아무것도 안 봤다. 그 사실을 안 적으면
166
+ * 모델은 이걸 프로젝트 전체 지도로 여기고 Outline 을 안 부른다 —
167
+ * 그러면 이 토막이 오히려 손해가 된다.
168
+ */
169
+ 줄들.push('위쪽 한 겹만 본 것이다. 안을 알아야 하면 Outline 을 불러라.');
170
+ return `\n--- 이 폴더 ---\n${줄들.join('\n')}`;
171
+ }
@@ -1,9 +1,12 @@
1
1
  // 대화 상태와 컨텍스트 셈. /context 가 보여주는 숫자가 여기서 나온다.
2
2
  import { readFileSync, existsSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
- import { get as workMode, DEFAULT as WORK_DEFAULT } from './modes.js';
4
+ import { get as workMode, 말 as 모드말, DEFAULT as WORK_DEFAULT } from './modes.js';
5
5
  import { toolSchemas } from '../tools/index.js';
6
6
  import { normalize as normLevel, DEFAULT as LEVEL_DEFAULT } from '../ui/level.js';
7
+ import { 매김, 급말, 값 as 급값, 지켜본것 } from './grade.js';
8
+ import { 지문 } from './project.js';
9
+ import { 프롬프트토막 as 기억토막 } from './memory.js';
7
10
 
8
11
  // 토큰 추정 — 정확한 토크나이저 없이 대략만 센다.
9
12
  // 한글은 글자당 약 1토큰, 영문·코드는 약 4글자당 1토큰으로 본다.
@@ -20,15 +23,21 @@ export function estimateTokens(text) {
20
23
 
21
24
  const BASE_RULES = `너는 deel 다. 사용자의 작업 폴더 안에서 코드를 읽고 고치는 도구다.
22
25
 
23
- 원칙:
24
- - 추측하지 말고 도구로 확인한다. 파일을 고치기 전에는 반드시 Read 로 읽는다.
26
+ 시킨 일을 **끝까지 해낸다.** 계획만 세우고 멈추지 않는다.
27
+
28
+ - 바로 시작한다. 없는 파일·폴더는 만든다. 그게 시킨 일의 일부다.
29
+ - 되묻는 것은 **도구로도 못 알아낼 때뿐**이다. 정할 수 있으면 정하고 무엇으로 정했는지 말한다.
30
+ - 여러 파일을 만들고 나눠 담아야 하는 일이면 그렇게 한다. 하나만 건드려 놓고 멈추지 마라.
31
+ - 다 했으면 확인한다. 돌려 보고 안 되면 고친다. 확인 못 했으면 "확인 못 했다" 고 말한다.
32
+
33
+ 지키는 것:
34
+ - 추측하지 말고 도구로 확인한다. 있는 파일을 고치기 전에는 반드시 Read 로 읽는다.
25
35
  - Edit 의 old_string 은 공백과 들여쓰기까지 파일과 정확히 같아야 한다. 짧게 자르지 말고 앞뒤로 넉넉히 포함한다.
26
- - 한 번에 하나씩 고치고, 고친 뒤에는 무엇을 왜 고쳤는지 한 줄로 말한다.
27
36
  - 큰 파일은 한 번에 다 담지 않는다. 앞부분 300줄쯤을 Write 로 만들고, 나머지는 Append 를
28
37
  여러 번 불러 끝까지 이어 붙인다. Append 는 Read 없이 바로 쓸 수 있다.
29
38
  이어 붙일 때 앞부분을 다시 보내지 않는다 — 그러면 또 같은 자리에서 잘린다.
30
39
  - 같은 도구를 같은 인자로 다시 부르지 않는다. 결과는 같다. 본 것은 기억하고 다음으로 넘어간다.
31
- - 사용자가 볼 범위를 좁혀 말하면 그대로 따른다. 시키지 않은 폴더를 뒤지지 않는다.
40
+ - 사용자가 볼 범위를 박아 말하면 범위를 지킨다. 그러면 필요한 만큼 찾아본다.
32
41
  - 명령 실행이 필요하면 Bash 를 쓴다. 되돌릴 수 없는 명령은 막히니 다른 방법을 찾는다.
33
42
  - 사용자에게 답할 때는 한국어로, 짧게. 코드를 통째로 붙여넣지 말고 무엇이 달라졌는지 말한다.
34
43
 
@@ -40,8 +49,44 @@ const BASE_RULES = `너는 deel 다. 사용자의 작업 폴더 안에서 코드
40
49
  앞머리에 name 과 description 을 넣고(--- 로 감싼다) 아래에 순서를 적는다.
41
50
  쓰던 스킬에서 틀린 데를 찾으면 그 파일을 고친다.`;
42
51
 
52
+ /*
53
+ * 작은 창을 위한 짧은 판.
54
+ *
55
+ * 같은 규칙이다 — 빠진 것은 없고, 설득하는 문장만 없다. 8k 모델에서 위의 긴
56
+ * 판은 창의 13% 를 먹는데, 그 자리는 대화가 써야 하는 자리다.
57
+ *
58
+ * 짧게 쓰되 **더 못 박아** 쓴다. 작은 모델이 못하는 것이 '긴 글을 끝까지
59
+ * 따라가기' 라서, 짧고 단정적인 쪽이 오히려 잘 지켜진다. (grade.js 도 같은
60
+ * 생각으로 되어 있다 — 거기는 급, 여기는 창 크기라는 점만 다르다.)
61
+ */
62
+ const BASE_RULES_짧게 = `너는 deel 다. 사용자의 작업 폴더에서 코드를 읽고 고친다.
63
+
64
+ 시킨 일을 끝까지 해낸다. 계획만 내고 멈추지 마라.
65
+ - 바로 시작한다. 없는 파일·폴더는 만든다.
66
+ - 도구로 알아낼 수 있으면 되묻지 말고 정한다. 무엇으로 정했는지는 말한다.
67
+ - 파일이 여럿이면 다 만든다. 하나만 하고 멈추지 마라.
68
+ - 끝내기 전에 Verify 로 확인한다. 확인 못 했으면 "확인 못 했다" 고 말한다.
69
+
70
+ - 고칠 파일은 먼저 Read 한다.
71
+ - Edit 의 old_string 은 공백까지 파일과 똑같아야 한다. 앞뒤를 넉넉히 넣어라.
72
+ - 긴 파일은 Write 로 앞부분만, 나머지는 Append 로 잇는다. 앞부분을 다시 보내지 마라.
73
+ - 같은 도구를 같은 인자로 또 부르지 마라. 결과는 같다.
74
+ - 답은 한국어로 짧게. 코드를 통째로 붙여넣지 마라.
75
+ - 사용자가 정한 규칙은 Remember 로 한 줄 남긴다. "저번에" 라고 하면 Recall 로 찾는다.`;
76
+
77
+ /**
78
+ * 이 창 크기에 맞는 기본 규칙.
79
+ *
80
+ * 24k 를 경계로 삼는다. 그 아래에서는 긴 판이 창의 10% 를 넘어가기 시작한다 —
81
+ * 도구 정의(budget.js 의 설명길이)가 줄어드는 자리와 같은 경계다. 두 개가
82
+ * 같이 움직여야 '작은 창에서는 고정 몫을 줄인다' 가 한 가지 결정이 된다.
83
+ */
84
+ function 기본규칙(ctx) {
85
+ return Number(ctx) > 0 && Number(ctx) < 24000 ? BASE_RULES_짧게 : BASE_RULES;
86
+ }
87
+
43
88
  export class Session {
44
- constructor(conn, { root, mode = 'auto', work = null, level = null, think = 'medium', effort = 'save', web = true, maxSteps = 24 } = {}) {
89
+ constructor(conn, { root, mode = 'auto', work = null, level = null, think = 'medium', effort = 'save', web = true, maxSteps = null } = {}) {
45
90
  this.conn = conn;
46
91
  this.root = root;
47
92
  this.mode = mode; // 승인 정책 — 얼마나 물어보나 (auto/confirm/strict)
@@ -54,7 +99,18 @@ export class Session {
54
99
  this.think = think; // 기준 강도
55
100
  this.effort = effort; // 그 강도를 단계별로 어떻게 나눌지 (effort.js)
56
101
  this.web = web; // 웹 읽기 도구를 줄지 (오프라인이면 무조건 안 준다)
57
- this.maxSteps = maxSteps;
102
+ /*
103
+ * 걸음 수 상한.
104
+ *
105
+ * 보통은 **작업 모드가 정한다** — 묻기는 8, 코드는 60, 총괄은 100 처럼
106
+ * 일의 성격에 맞는 값이 다르기 때문이다. 여기 값은 부를 때 직접 준 경우에만
107
+ * 이긴다(loop.js 가 stepsSet 을 본다).
108
+ *
109
+ * 전에는 stepsSet 을 아무 데서도 안 넣어서, 직접 준 값이 **조용히 무시**됐다.
110
+ * 부르는 쪽에서는 4를 줬는데 60을 도는 식이라, 검사에서야 겨우 드러났다.
111
+ */
112
+ this.stepsSet = maxSteps != null;
113
+ this.maxSteps = maxSteps ?? 24;
58
114
  this.messages = [];
59
115
  this.filesRead = new Map(); // 경로 → 추정 토큰
60
116
  this.changes = new Map(); // 경로 → {added, removed, times}. /diff 가 본다
@@ -64,8 +120,36 @@ export class Session {
64
120
  this.maxSkillsListed = 40; // 프롬프트에 올릴 최대 개수
65
121
  this.maxSkillDesc = 140; // 설명 한 줄 최대 길이
66
122
  this.usage = { in: 0, out: 0, calls: 0, ms: 0 };
123
+ /*
124
+ * 지금 붙은 모델이 얼마나 하는가 (agent/grade.js).
125
+ *
126
+ * 창 크기와는 다른 축이다. 창은 '얼마나 담나', 급은 '얼마나 알아서 하나'.
127
+ * 128k 짜리 3B 모델과 32k 짜리 좋은 모델을 같은 값으로 다루면 둘 다 손해다.
128
+ *
129
+ * 처음에는 이름으로 짐작하고, 대화가 돌수록 **실제로 본 것**으로 고쳐 잡는다.
130
+ * 사람이 /grade 로 정하면 그것이 이긴다.
131
+ */
132
+ this.본것 = new 지켜본것();
133
+ this.급정한것 = null;
67
134
  this.startedAt = Date.now();
68
135
  this.rules = this.#loadRules();
136
+ /*
137
+ * 이 폴더가 무슨 프로젝트인가 (agent/project.js).
138
+ *
139
+ * 규칙(DEEL.md)과 같은 자리에서 읽는다 — 켤 때 한 번이다. 매 턴 다시 읽으면
140
+ * 긴 대화에서 수십 번이 되고, 그 사이 사람이 package.json 을 고쳐 놓으면
141
+ * 대화 도중에 프롬프트가 바뀐다. 무엇 때문에 답이 달라졌는지 알 길이 없어진다.
142
+ */
143
+ this.프로젝트 = 지문(this.root, this.conn?.ctx ?? null);
144
+ /*
145
+ * 지난 대화에서 정한 것도 여기서 읽는다.
146
+ *
147
+ * 전에는 대화 화면(repl.js)에서만 넣었다. 그래서 `deel run` — 야간 배치로
148
+ * 도는 쪽 — 에는 기억이 안 실렸다. "우리 문서는 CP949 다" 를 사람이 앉아
149
+ * 있을 때만 지키고 배치에서는 안 지키는 셈이라, 그게 제일 나쁜 어긋남이다.
150
+ * 규칙과 같은 자리로 옮겨서 두 길이 같은 것을 들고 시작하게 한다.
151
+ */
152
+ this.memory = 기억토막(this.root);
69
153
  }
70
154
 
71
155
  #loadRules() {
@@ -89,13 +173,38 @@ export class Session {
89
173
  return this.routed ?? this.work;
90
174
  }
91
175
 
176
+ /** 지금 매겨진 모델 급. 화면과 프롬프트가 같은 것을 봐야 한다. */
177
+ 급() { return 매김(this.conn, this.본것, this.급정한것); }
178
+
179
+ /** 이 급에서 쓸 손잡이 값들 (한 번에 만들 파일 수 같은 것). */
180
+ 급값() { return 급값(this.급().급); }
181
+
92
182
  systemPrompt() {
93
- const parts = [BASE_RULES];
183
+ const parts = [기본규칙(this.conn?.ctx)];
94
184
  parts.push(`\n작업 폴더: ${this.root}\n이 폴더 밖의 파일은 읽지도 쓰지도 못한다.`);
95
185
 
96
186
  // 지금 무슨 일을 하는 중인지. 도구 목록도 이 모드에 맞춰 이미 걸러져 있다.
97
187
  const w = workMode(this.effectiveWork());
98
- parts.push(`\n--- 지금 모드: ${w.name} (${w.en}) ---\n${w.say}`);
188
+ // 창이 좁으면 짧은 판을 쓴다 (modes.js 의 말()). 규칙은 같고 설득하는 문장만 빠진다.
189
+ parts.push(`\n--- 지금 모드: ${w.name} (${w.en}) ---\n${모드말(this.effectiveWork(), this.conn?.ctx)}`);
190
+ /*
191
+ * 모델 급에 맞춘 한 문단 (grade.js).
192
+ *
193
+ * 큰 모델에는 아무것도 안 붙는다 — 이미 아는 것을 다시 읽느라 자리만 먹는다.
194
+ * 작은 모델에만, 짧게, 못 박아서 붙는다. 그 급이 못하는 것이 바로
195
+ * '긴 글을 끝까지 따라가기' 라서, 길게 쓰면 오히려 나빠진다.
196
+ */
197
+ const 급글 = 급말(this.급().급);
198
+ if (급글) parts.push(`\n${급글}`);
199
+
200
+ /*
201
+ * 이 폴더가 무슨 프로젝트인가 (agent/project.js).
202
+ *
203
+ * 규칙보다 **앞에** 둔다. 사용자 규칙은 "이 프로젝트에서는 이렇게 해라" 는
204
+ * 말이라, 무슨 프로젝트인지를 먼저 읽은 뒤에 와야 말이 이어진다.
205
+ */
206
+ if (this.프로젝트) parts.push(this.프로젝트);
207
+
99
208
  if (this.rules) parts.push(`\n--- ${this.rules.name} (사용자 규칙, 위 원칙보다 우선) ---\n${this.rules.text}`);
100
209
 
101
210
  /*
@@ -174,8 +283,12 @@ export class Session {
174
283
  * · 지금 모드 문구 — modes.js 의 say. 모드마다 수백 토큰이다.
175
284
  */
176
285
  breakdown() {
177
- const sys = estimateTokens(BASE_RULES) + estimateTokens(`작업 폴더: ${this.root}`)
178
- + estimateTokens(workMode(this.effectiveWork()).say ?? '');
286
+ // 폴더 지문도 요청에 통째로 나간다. 시스템 프롬프트 쪽에 같이 센다 —
287
+ // 세면 '남은 자리' 가 그만큼 뻥튀기되고, effort.js 가 그 값으로 출력
288
+ // 상한을 잡으므로 답이 조용히 잘리기 시작한다.
289
+ const sys = estimateTokens(기본규칙(this.conn?.ctx)) + estimateTokens(`작업 폴더: ${this.root}`)
290
+ + estimateTokens(모드말(this.effectiveWork(), this.conn?.ctx) ?? '')
291
+ + estimateTokens(this.프로젝트 ?? '');
179
292
  const rules = this.rules ? estimateTokens(this.rules.text) : 0;
180
293
  const listed = this.listedSkills();
181
294
  const skills = listed.length
@@ -222,7 +335,9 @@ export class Session {
222
335
  #도구토큰() {
223
336
  // 밖에서 붙인 도구 수까지 열쇠에 넣는다. 서버가 붙고 떨어지면 값이 달라진다.
224
337
  const mcp수 = (this.mcp ?? []).reduce((n, s) => n + (s.도구?.length ?? 0), 0);
225
- const 열쇠 = `${this.effectiveWork()}|${this.skills?.length ? 'skill' : ''}|${this.web !== false ? 'web' : ''}|mcp${mcp수}`;
338
+ // 크기도 열쇠에 넣는다. 설명을 창에 맞춰 줄여 싣기 때문에(budget.js),
339
+ // /ctx 로 창을 다시 잡으면 이 값도 달라져야 한다. 안 넣으면 옛 값이 남는다.
340
+ const 열쇠 = `${this.effectiveWork()}|${this.skills?.length ? 'skill' : ''}|${this.web !== false ? 'web' : ''}|mcp${mcp수}|c${this.conn?.ctx ?? 0}`;
226
341
  if (this.#도구잰것.has(열쇠)) return this.#도구잰것.get(열쇠);
227
342
  let n = 0;
228
343
  try {
@@ -231,6 +346,10 @@ export class Session {
231
346
  web: this.web !== false,
232
347
  work: this.effectiveWork(),
233
348
  mcp: this.mcp ?? null,
349
+ // 실제로 나가는 것과 **같은 것**을 재야 한다. 안 넘기면 안 줄인 것을
350
+ // 재게 되고, 그러면 /context 가 실제보다 크게 말한다 — 그 값으로
351
+ // effort.js 가 출력 상한을 잡으므로 답이 이유 없이 짧아진다.
352
+ ctx: this.conn?.ctx ?? null,
234
353
  });
235
354
  n = estimateTokens(JSON.stringify(list));
236
355
  } catch { n = 0; }