deel-local-cli 0.5.0 → 0.9.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.
- package/README.en.md +738 -132
- package/README.md +742 -147
- package/bin/deel.js +96 -4
- package/package.json +4 -3
- package/src/agent/compact.js +138 -0
- package/src/agent/effort.js +139 -0
- package/src/agent/loop.js +186 -60
- package/src/agent/modes.js +252 -0
- package/src/agent/route.js +156 -0
- package/src/agent/session.js +28 -3
- package/src/agent/sessionui.js +59 -0
- package/src/agent/store.js +193 -0
- package/src/backend/adapter.js +12 -2
- package/src/backend/ctxsize.js +174 -0
- package/src/backend/http.js +44 -3
- package/src/backend/probe.js +15 -14
- package/src/backend/scan.js +256 -0
- package/src/backend/scanui.js +147 -0
- package/src/commands.js +658 -33
- package/src/config.js +19 -6
- package/src/pack/selfpack.js +248 -0
- package/src/pack/tar.js +69 -0
- package/src/pack/zip.js +217 -0
- package/src/plugins/manage.js +272 -0
- package/src/repl.js +297 -40
- package/src/report.js +1 -1
- package/src/safety/network.js +95 -0
- package/src/safety/undo.js +46 -1
- package/src/setup.js +4 -0
- package/src/tools/encoding.js +333 -0
- package/src/tools/excel-com.js +254 -0
- package/src/tools/excel.js +118 -0
- package/src/tools/fsutil.js +47 -4
- package/src/tools/index.js +128 -14
- package/src/tools/todo.js +92 -0
- package/src/tools/webfetch.js +110 -0
- package/src/tools/xlsx.js +319 -0
- package/src/ui/ansi.js +97 -5
- package/src/ui/level.js +106 -0
- package/src/ui/prompt.js +34 -0
- package/src/ui/status.js +161 -0
package/src/agent/loop.js
CHANGED
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
import { chat, chatStream, assistantMessage, toolMessage } from '../backend/adapter.js';
|
|
4
4
|
import { toolSchemas, runTool, TOOLS } from '../tools/index.js';
|
|
5
5
|
import { isMutating } from '../safety/guard.js';
|
|
6
|
+
import { effortFor, tokensFor, fullCap, wasCut } from './effort.js';
|
|
7
|
+
import { compact, shouldCompact } from './compact.js';
|
|
8
|
+
import { isOffline } from '../safety/network.js';
|
|
9
|
+
import { get as workMode } from './modes.js';
|
|
6
10
|
|
|
7
11
|
// think 값을 규격에 맞게. 'off' 는 사고를 끈다.
|
|
8
12
|
function thinkFor(conn, level) {
|
|
@@ -10,39 +14,121 @@ function thinkFor(conn, level) {
|
|
|
10
14
|
return level;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
// 아무것도 안 바꾸는 도구들. 이것들만 동시에 돌린다.
|
|
18
|
+
//
|
|
19
|
+
// 왜 이것만인가:
|
|
20
|
+
// Read 세 개를 동시에 하는 것은 안전하다 — 서로 안 건드린다.
|
|
21
|
+
// Write·Edit 을 동시에 돌리면 같은 파일을 두 갈래로 고칠 수 있고,
|
|
22
|
+
// 되돌리기 스냅샷 순서도 뒤엉킨다. Bash 는 무슨 짓을 할지 알 수 없다.
|
|
23
|
+
// 그래서 '읽기만 하는 것' 이라고 확실한 도구만 묶는다.
|
|
24
|
+
const 읽기전용 = new Set(['Read', 'Glob', 'Grep', 'Skill', 'WebFetch']);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 호출 목록을 '같이 돌려도 되는 덩어리' 로 자른다.
|
|
28
|
+
* 읽기 전용이 이어지면 한 덩어리, 그 밖의 것은 하나씩 따로.
|
|
29
|
+
*/
|
|
30
|
+
export function 묶기(calls) {
|
|
31
|
+
const out = [];
|
|
32
|
+
for (const call of calls) {
|
|
33
|
+
const 안전 = 읽기전용.has(call.name);
|
|
34
|
+
const 끝 = out.at(-1);
|
|
35
|
+
if (안전 && 끝?.parallel) 끝.calls.push(call);
|
|
36
|
+
else out.push({ parallel: 안전, calls: [call] });
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function* run(session, ctx, userText, { signal = null } = {}) {
|
|
14
42
|
session.push({ role: 'user', content: userText });
|
|
15
43
|
ctx.audit.turn(userText);
|
|
16
44
|
ctx.history.nextTurn();
|
|
17
45
|
|
|
18
46
|
const conn = session.conn;
|
|
19
|
-
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 중단하고 나갈 때 대화를 성한 상태로 남긴다.
|
|
50
|
+
*
|
|
51
|
+
* 모델이 도구를 부르겠다고 해 놓고 결과가 안 들어간 채로 끝나면, 다음에 이어할 때
|
|
52
|
+
* 그 배열을 그대로 보낼 수 없다 — 서버가 400 을 낸다. 그래서 부른 만큼
|
|
53
|
+
* '중단됨' 결과를 채워 짝을 맞춘다.
|
|
54
|
+
*/
|
|
55
|
+
const 짝맞추기 = () => {
|
|
56
|
+
const last = session.messages.at(-1);
|
|
57
|
+
const calls = last?.tool_calls ?? [];
|
|
58
|
+
if (!calls.length) return;
|
|
59
|
+
const 이미 = session.messages.filter((m) => m.role === 'tool').length;
|
|
60
|
+
for (const t of calls) {
|
|
61
|
+
session.push(toolMessage(conn.kind, {
|
|
62
|
+
callId: t.id ?? `call_${이미 + 1}`,
|
|
63
|
+
name: t.function?.name ?? t.name ?? '?',
|
|
64
|
+
content: '사용자가 중단했습니다. 실행하지 않았습니다.',
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
const tools = toolSchemas(null, {
|
|
69
|
+
hasSkills: (session.skills?.length ?? 0) > 0,
|
|
70
|
+
web: session.web !== false && !isOffline(), // 오프라인이면 웹 도구는 아예 안 보여 준다
|
|
71
|
+
work: session.effectiveWork(), // 작업 모드가 쓰는 것만 (modes.js)
|
|
72
|
+
});
|
|
73
|
+
// 모드마다 생각의 배분과 걸음 수가 다르다. 사용자가 따로 정했으면 그걸 존중한다.
|
|
74
|
+
const 모드 = workMode(session.effectiveWork());
|
|
75
|
+
const effort = session.effortSet ? session.effort : (모드.effort ?? session.effort);
|
|
76
|
+
const think = session.thinkSet ? session.think : (모드.think ?? session.think);
|
|
77
|
+
const maxSteps = session.stepsSet ? session.maxSteps : (모드.steps ?? session.maxSteps);
|
|
20
78
|
const attempted = new Set(); // 같은 변경성 명령을 두 번 실행하지 않기 위한 기록
|
|
21
79
|
let steps = 0;
|
|
80
|
+
let lastToolFailed = false; // 직전 단계에서 도구가 오류를 냈나 → 다음 판단은 세게
|
|
22
81
|
|
|
23
|
-
while (steps <
|
|
82
|
+
while (steps < maxSteps) {
|
|
24
83
|
steps++;
|
|
25
|
-
|
|
84
|
+
// 단계마다 필요한 생각의 양이 다르다. effort.js 가 그 배분을 갖고 있다.
|
|
85
|
+
const stage = steps === 1 ? 'plan' : lastToolFailed ? 'fix' : 'work';
|
|
86
|
+
const level = effortFor(think, effort, stage);
|
|
87
|
+
// 상한은 모델 컨텍스트와 지금 찬 양에서 계산한다. 고정 숫자가 아니다.
|
|
88
|
+
const room = { ctx: conn.ctx ?? 0, used: session.breakdown().used, max: conn.maxTokens ?? null };
|
|
89
|
+
const cap = tokensFor(effort, stage, room);
|
|
90
|
+
yield { type: 'stage', stage, level, cap, step: steps };
|
|
91
|
+
|
|
92
|
+
const ask = (maxTokens, think) => ({
|
|
26
93
|
messages: session.wire(),
|
|
27
94
|
tools,
|
|
28
|
-
think: thinkFor(conn,
|
|
29
|
-
maxTokens
|
|
30
|
-
|
|
95
|
+
think: thinkFor(conn, think),
|
|
96
|
+
maxTokens,
|
|
97
|
+
signal,
|
|
98
|
+
});
|
|
31
99
|
|
|
32
100
|
let msg;
|
|
33
|
-
|
|
101
|
+
const askModel = async function* (maxTokens, think) {
|
|
34
102
|
if (conn.streaming) {
|
|
35
|
-
for await (const ev of chatStream(conn,
|
|
103
|
+
for await (const ev of chatStream(conn, ask(maxTokens, think))) {
|
|
36
104
|
if (ev.type === 'done') msg = ev.message;
|
|
37
105
|
else yield ev;
|
|
38
106
|
}
|
|
39
107
|
} else {
|
|
40
108
|
yield { type: 'waiting' };
|
|
41
|
-
msg = await chat(conn,
|
|
109
|
+
msg = await chat(conn, ask(maxTokens, think));
|
|
42
110
|
if (msg.thinking) yield { type: 'thinking', text: msg.thinking };
|
|
43
111
|
if (msg.content) yield { type: 'content', text: msg.content };
|
|
44
112
|
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
yield* askModel(cap, level);
|
|
117
|
+
|
|
118
|
+
// 아낀 상한 때문에 대답이 잘렸다면, 그 단계만 상한을 풀어 한 번 다시 부른다.
|
|
119
|
+
// 잘린 채로 넘어가면 도구 호출이 반토막 나서 조용히 실패한다 —
|
|
120
|
+
// 생각을 많이 하는 모델에서 특히 잘 생긴다.
|
|
121
|
+
const full = fullCap(room);
|
|
122
|
+
if (wasCut(msg) && cap < full) {
|
|
123
|
+
yield { type: 'retry', why: '대답이 상한에서 잘렸습니다', from: cap, to: full };
|
|
124
|
+
yield* askModel(full, level);
|
|
125
|
+
}
|
|
45
126
|
} catch (err) {
|
|
127
|
+
if (err?.name === 'Aborted' || signal?.aborted) {
|
|
128
|
+
짝맞추기();
|
|
129
|
+
yield { type: 'aborted', steps };
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
46
132
|
yield { type: 'error', text: err.message };
|
|
47
133
|
return;
|
|
48
134
|
}
|
|
@@ -58,66 +144,106 @@ export async function* run(session, ctx, userText) {
|
|
|
58
144
|
return;
|
|
59
145
|
}
|
|
60
146
|
|
|
61
|
-
// 도구를
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}));
|
|
68
|
-
yield { type: 'tool', name: call.name, args: call.args, result: { error: '모르는 도구' } };
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
147
|
+
// 도구를 돌린다. 읽기만 하는 것들이 이어지면 한꺼번에, 나머지는 하나씩.
|
|
148
|
+
lastToolFailed = false;
|
|
149
|
+
const 거절 = (call, note) => {
|
|
150
|
+
lastToolFailed = true;
|
|
151
|
+
session.push(toolMessage(conn.kind, { callId: call.id, name: call.name, content: note }));
|
|
152
|
+
};
|
|
71
153
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
session.push(toolMessage(conn.kind, {
|
|
78
|
-
|
|
79
|
-
|
|
154
|
+
for (const 덩어리 of 묶기(msg.toolCalls)) {
|
|
155
|
+
// 돌리는 중에 끊었다면, 남은 것은 실행하지 않고 결과 자리만 채운다.
|
|
156
|
+
// 자리를 비우면 짝이 깨져 다음에 이어할 수 없다.
|
|
157
|
+
if (signal?.aborted) {
|
|
158
|
+
for (const call of 덩어리.calls) {
|
|
159
|
+
session.push(toolMessage(conn.kind, {
|
|
160
|
+
callId: call.id, name: call.name,
|
|
161
|
+
content: '사용자가 중단했습니다. 실행하지 않았습니다.',
|
|
162
|
+
}));
|
|
80
163
|
}
|
|
81
|
-
|
|
164
|
+
continue;
|
|
82
165
|
}
|
|
83
166
|
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
:
|
|
90
|
-
if (needsOk && ctx.confirm) {
|
|
91
|
-
const ok = await ctx.confirm(call.name, call.args);
|
|
92
|
-
if (!ok) {
|
|
93
|
-
const note = '사용자가 거부했습니다. 다른 방법을 찾거나 이유를 물어보세요.';
|
|
94
|
-
session.push(toolMessage(conn.kind, { callId: call.id, name: call.name, content: note }));
|
|
95
|
-
yield { type: 'tool', name: call.name, args: call.args, result: { error: '거부됨' } };
|
|
167
|
+
// 먼저 하나씩 걸러 낸다 — 물어보는 것도 여기서. 실제 실행은 통과한 것만.
|
|
168
|
+
const 실행할것 = [];
|
|
169
|
+
for (const call of 덩어리.calls) {
|
|
170
|
+
if (!TOOLS[call.name]) {
|
|
171
|
+
거절(call, `모르는 도구입니다. 쓸 수 있는 것: ${Object.keys(TOOLS).join(', ')}`);
|
|
172
|
+
yield { type: 'tool', name: call.name, args: call.args, result: { error: '모르는 도구' } };
|
|
96
173
|
continue;
|
|
97
174
|
}
|
|
98
|
-
}
|
|
99
175
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
176
|
+
// 변경성 명령은 실패해도 다시 실행하지 않는다 — 두 번 돌면 사고다.
|
|
177
|
+
if (call.name === 'Bash' && isMutating(call.args?.command)) {
|
|
178
|
+
const key = String(call.args.command).trim();
|
|
179
|
+
if (attempted.has(key)) {
|
|
180
|
+
const note = '같은 변경성 명령을 다시 실행하지 않습니다. 두 번 실행되면 사고가 납니다.';
|
|
181
|
+
거절(call, note);
|
|
182
|
+
yield { type: 'tool', name: call.name, args: call.args, result: { error: note } };
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
attempted.add(key);
|
|
186
|
+
}
|
|
107
187
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
188
|
+
// 모드에 따라 물어본다. 기본(auto)은 안 묻고 되돌리기로 대응한다.
|
|
189
|
+
const needsOk = session.mode === 'strict'
|
|
190
|
+
? ['Write', 'Edit', 'Bash'].includes(call.name)
|
|
191
|
+
: session.mode === 'confirm'
|
|
192
|
+
? (call.name === 'Bash' && isMutating(call.args?.command))
|
|
193
|
+
: false;
|
|
194
|
+
if (needsOk && ctx.confirm) {
|
|
195
|
+
const ok = await ctx.confirm(call.name, call.args);
|
|
196
|
+
if (!ok) {
|
|
197
|
+
거절(call, '사용자가 거부했습니다. 다른 방법을 찾거나 이유를 물어보세요.');
|
|
198
|
+
yield { type: 'tool', name: call.name, args: call.args, result: { error: '거부됨' } };
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
실행할것.push(call);
|
|
203
|
+
}
|
|
204
|
+
if (!실행할것.length) continue;
|
|
205
|
+
|
|
206
|
+
// 여럿을 같이 돌릴 때는 '시작' 을 따로 알리지 않는다.
|
|
207
|
+
// 화면에서 이름 셋이 먼저 뜨고 결과 셋이 뒤에 몰려 붙으면, 어느 결과가
|
|
208
|
+
// 어느 파일 것인지 읽을 수 없다. 그럴 때는 끝난 것부터 이름과 결과를 함께 그린다.
|
|
209
|
+
const 함께 = 실행할것.length > 1;
|
|
210
|
+
if (!함께) yield { type: 'tool_start', name: 실행할것[0].name, args: 실행할것[0].args };
|
|
211
|
+
else yield { type: 'tools_start', names: 실행할것.map((x) => x.name), count: 실행할것.length };
|
|
212
|
+
|
|
213
|
+
const 한개 = async (call) => {
|
|
214
|
+
const t0 = Date.now();
|
|
215
|
+
let result;
|
|
216
|
+
try { result = await runTool(call.name, call.args, ctx); }
|
|
217
|
+
catch (err) { result = { error: String(err?.message ?? err) }; }
|
|
218
|
+
return { call, result, ms: Date.now() - t0 };
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// 여럿이면 동시에. 읽기만 하는 것들이라 서로 방해하지 않는다.
|
|
222
|
+
const 결과들 = 함께
|
|
223
|
+
? await Promise.all(실행할것.map(한개))
|
|
224
|
+
: [await 한개(실행할것[0])];
|
|
225
|
+
|
|
226
|
+
for (const { call, result, ms } of 결과들) {
|
|
227
|
+
session.usage.ms += ms;
|
|
228
|
+
if (result.error) lastToolFailed = true;
|
|
229
|
+
if (call.name === 'Read' && result.content) session.noteRead(call.args.file_path, result.content);
|
|
230
|
+
session.push(toolMessage(conn.kind, {
|
|
231
|
+
callId: call.id,
|
|
232
|
+
name: call.name,
|
|
233
|
+
content: result.error ? `오류: ${result.error}` : result.content ?? '',
|
|
234
|
+
}));
|
|
235
|
+
yield { type: 'tool', name: call.name, args: call.args, result, ms, parallel: 함께 };
|
|
236
|
+
}
|
|
114
237
|
}
|
|
115
238
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
239
|
+
if (signal?.aborted) { yield { type: 'aborted', steps }; return; }
|
|
240
|
+
|
|
241
|
+
// 컨텍스트가 차오르면 오래된 대화를 '요약해서' 접는다. 그냥 자르면 하던 일을 잊는다.
|
|
242
|
+
if (shouldCompact(session)) {
|
|
243
|
+
yield { type: 'compacting' };
|
|
244
|
+
const r = await compact(session, { auto: true });
|
|
245
|
+
if (r.ok) yield { type: 'compacted', ...r };
|
|
246
|
+
else yield { type: 'compact_failed', why: r.why };
|
|
121
247
|
}
|
|
122
248
|
}
|
|
123
249
|
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// 작업 모드. 지금 무슨 일을 하는 중인지에 따라 도구·추론·말투를 한꺼번에 바꾼다.
|
|
2
|
+
//
|
|
3
|
+
// 승인 정책(auto/confirm/strict)과는 다른 축이다. 헷갈리면 안 된다.
|
|
4
|
+
// 승인 정책 = 얼마나 물어보나
|
|
5
|
+
// 작업 모드 = 무슨 일을 하는 중인가
|
|
6
|
+
// 둘은 곱해진다. '설계 모드 + strict' 도, '코드 모드 + auto' 도 말이 된다.
|
|
7
|
+
//
|
|
8
|
+
// 왜 도구를 아예 빼는가:
|
|
9
|
+
// 설계 모드에서 파일을 고치면 안 된다고 프롬프트로 부탁할 수도 있다.
|
|
10
|
+
// 그런데 모델은 부탁을 잊는다. 아예 목록에서 빼면 잊을 것이 없다.
|
|
11
|
+
// 오프라인일 때 웹 도구를 숨기는 것과 같은 방식이다.
|
|
12
|
+
|
|
13
|
+
// 읽기만 하는 도구. 무엇을 바꾸지 않는다.
|
|
14
|
+
const 읽기 = ['Read', 'Glob', 'Grep', 'WebFetch', 'Skill'];
|
|
15
|
+
// 계획을 적는 도구. 파일을 안 건드리므로 읽기 전용 모드에서도 준다.
|
|
16
|
+
const 계획 = ['TodoWrite'];
|
|
17
|
+
// 바꾸는 도구.
|
|
18
|
+
const 쓰기 = ['Write', 'Edit', 'Bash'];
|
|
19
|
+
|
|
20
|
+
export const MODES = {
|
|
21
|
+
// 처음에는 여기서 시작한다.
|
|
22
|
+
//
|
|
23
|
+
// 무슨 일을 시킬지 미리 정해 두고 켜는 사람은 드물다. 그래서 기본은 '종합' 이고,
|
|
24
|
+
// 한마디를 할 때마다 그 말이 무슨 일인지 보고 알맞은 모드로 저절로 옮겨 간다
|
|
25
|
+
// (agent/route.js). 애매하면 안 옮기고 여기 그대로 둔다.
|
|
26
|
+
//
|
|
27
|
+
// 사용자가 /plan 처럼 직접 고르면 저절로 옮기는 것은 멈춘다. 사람이 고른 것을
|
|
28
|
+
// 뒤집지 않는다. 다시 맡기려면 /work 종합 이다.
|
|
29
|
+
auto: {
|
|
30
|
+
id: 'auto',
|
|
31
|
+
name: '종합',
|
|
32
|
+
en: 'Auto',
|
|
33
|
+
glyph: '◎',
|
|
34
|
+
hint: '요청에 따라 알맞은 모드로',
|
|
35
|
+
tools: [...읽기, ...계획, ...쓰기],
|
|
36
|
+
effort: 'save',
|
|
37
|
+
think: null,
|
|
38
|
+
steps: 24,
|
|
39
|
+
say: [
|
|
40
|
+
'지금은 **종합** 모드다. 무슨 일이 올지 정해져 있지 않다.',
|
|
41
|
+
'',
|
|
42
|
+
'- 시키는 일이 무엇인지 먼저 가늠하고, 그에 맞는 방식으로 해라.',
|
|
43
|
+
' 고치는 일이면 읽고 나서 고치고, 원인을 찾는 일이면 확인부터 하고,',
|
|
44
|
+
' 설명하는 일이면 근거를 파일에서 대라.',
|
|
45
|
+
'- 큰 일이면 TodoWrite 로 쪼개 적고 하나씩 해라.',
|
|
46
|
+
'- 확인할 수 있는 것은 확인해라. 확인 못 한 것을 됐다고 하지 마라.',
|
|
47
|
+
'- 시키지 않은 일을 벌이지 마라.',
|
|
48
|
+
].join('\n'),
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
code: {
|
|
52
|
+
id: 'code',
|
|
53
|
+
name: '코드',
|
|
54
|
+
en: 'Code',
|
|
55
|
+
glyph: '◆',
|
|
56
|
+
hint: '고치고 만든다',
|
|
57
|
+
tools: [...읽기, ...계획, ...쓰기],
|
|
58
|
+
effort: 'save', // 첫 판단만 세게, 이어가기는 얕게
|
|
59
|
+
think: null, // 사용자가 정한 값을 그대로 쓴다
|
|
60
|
+
steps: 24,
|
|
61
|
+
say: [
|
|
62
|
+
"지금 하는 일은 **구현**이다. 아래 순서를 지켜라.",
|
|
63
|
+
"",
|
|
64
|
+
"1. 고칠 파일을 먼저 Read 로 읽는다. 안 읽고 고치려 하면 도구가 거절한다.",
|
|
65
|
+
"2. 주변 코드의 관례를 따른다 — 이름 짓는 법, 오류 다루는 법, 주석 밀도까지.",
|
|
66
|
+
" 새 관례를 들여오지 마라. 이 코드가 이미 하고 있는 대로 한다.",
|
|
67
|
+
"3. 한 번에 하나씩 고친다. 여러 파일에 흩뿌리지 마라.",
|
|
68
|
+
"4. 고친 뒤에는 확인할 방법이 있으면 실제로 돌려 본다 (검사·빌드·실행).",
|
|
69
|
+
" 확인 못 했으면 \"확인 못 했다\" 고 말한다. 됐다고 하지 마라.",
|
|
70
|
+
"5. 끝나면 무엇을 왜 바꿨는지 한두 줄로 말한다. 코드를 통째로 붙여넣지 마라.",
|
|
71
|
+
].join('\n'),
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
architect: {
|
|
75
|
+
id: 'architect',
|
|
76
|
+
name: '설계',
|
|
77
|
+
en: 'Architect',
|
|
78
|
+
glyph: '◈',
|
|
79
|
+
hint: '구조를 짠다 · 파일은 안 건드림',
|
|
80
|
+
tools: [...읽기, ...계획],
|
|
81
|
+
effort: 'deep',
|
|
82
|
+
think: 'high',
|
|
83
|
+
steps: 20,
|
|
84
|
+
say: [
|
|
85
|
+
"지금 하는 일은 **설계**다. 파일을 바꾸는 도구는 주어지지 않았다.",
|
|
86
|
+
"",
|
|
87
|
+
"먼저 읽어라. 지금 구조를 모르면 설계가 아니라 상상이다.",
|
|
88
|
+
" - 관련된 파일을 Glob/Grep 으로 찾고 Read 로 실제로 읽는다",
|
|
89
|
+
" - 무엇이 무엇에 기대고 있는지(의존 방향)를 파악한다",
|
|
90
|
+
"",
|
|
91
|
+
"그 다음 이 차례로 답한다.",
|
|
92
|
+
" 1. 지금 구조 — 파일과 역할, 경계가 어디인지 (경로:줄 로 짚어라)",
|
|
93
|
+
" 2. 무엇이 문제인가 — 왜 지금 모양으로는 안 되는지",
|
|
94
|
+
" 3. 선택지 2~3개 — 각각 무엇을 얻고 무엇을 잃는지, 손이 얼마나 가는지",
|
|
95
|
+
" 4. 추천 하나와 그 이유, 버린 것을 왜 버렸는지",
|
|
96
|
+
" 5. 영향받는 파일 목록",
|
|
97
|
+
"",
|
|
98
|
+
"이 코드가 이미 쓰는 관례 안에서 답을 찾아라. 새 틀을 들여오는 것은 마지막",
|
|
99
|
+
"수단이고, 그때는 왜 기존 관례로 안 되는지를 먼저 밝혀라.",
|
|
100
|
+
].join('\n'),
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
ask: {
|
|
104
|
+
id: 'ask',
|
|
105
|
+
name: '묻기',
|
|
106
|
+
en: 'Ask',
|
|
107
|
+
glyph: '◇',
|
|
108
|
+
hint: '설명만 · 아무것도 안 바꿈',
|
|
109
|
+
tools: [...읽기],
|
|
110
|
+
effort: 'even',
|
|
111
|
+
think: 'low',
|
|
112
|
+
steps: 8,
|
|
113
|
+
say: [
|
|
114
|
+
"지금 하는 일은 **설명**이다. 아무것도 바꾸지 않는다.",
|
|
115
|
+
"",
|
|
116
|
+
"- 근거를 파일에서 대라. 경로와 줄 번호를 같이 적어라 (src/a.js:42).",
|
|
117
|
+
"- 모르면 모른다고 하라. 그럴듯한 답을 지어내지 마라.",
|
|
118
|
+
"- 짧게, 물어본 것에만 답하라.",
|
|
119
|
+
"- 고치라는 말이 없으면 고칠 것을 제안하지 마라. 시키지 않은 일을 벌이지 마라.",
|
|
120
|
+
].join('\n'),
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
debug: {
|
|
124
|
+
id: 'debug',
|
|
125
|
+
name: '디버그',
|
|
126
|
+
en: 'Debug',
|
|
127
|
+
glyph: '◉',
|
|
128
|
+
hint: '원인을 찾는다',
|
|
129
|
+
tools: [...읽기, ...계획, ...쓰기],
|
|
130
|
+
effort: 'deep',
|
|
131
|
+
think: 'high',
|
|
132
|
+
steps: 32, // 원인 찾기는 왔다 갔다 하므로 여유를 준다
|
|
133
|
+
say: [
|
|
134
|
+
"지금 하는 일은 **원인 찾기**다. 짐작으로 고치지 마라.",
|
|
135
|
+
"",
|
|
136
|
+
"이 순서를 지켜라.",
|
|
137
|
+
" 1. 증상을 한 문장으로 다시 적는다 — 무엇을 했을 때 무엇이 일어나는가.",
|
|
138
|
+
" 2. 재현 방법을 확보한다. 없으면 만든다. 재현 못 하면 고쳤는지도 알 수 없다.",
|
|
139
|
+
" 3. 가설을 2~3개 세운다. 각각 \"이게 맞다면 무엇이 보여야 하는가\" 를 같이 적는다.",
|
|
140
|
+
" 4. 하나씩 실제로 확인한다 — 로그를 보고, 작게 돌려 보고, 값을 찍어 본다.",
|
|
141
|
+
" 확인한 것만 사실이다. 확인 안 한 것은 아직 가설이다.",
|
|
142
|
+
" 5. 원인을 짚을 때는 증거를 같이 댄다. \"아마\" 로 시작하는 원인은 원인이 아니다.",
|
|
143
|
+
" 6. 고친 뒤 2번의 재현 절차로 다시 확인한다. 안 고쳐졌으면 3번으로 돌아간다.",
|
|
144
|
+
"",
|
|
145
|
+
"한 번에 여러 곳을 고치지 마라. 무엇이 고쳤는지 알 수 없게 된다.",
|
|
146
|
+
].join('\n'),
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
plan: {
|
|
150
|
+
id: 'plan',
|
|
151
|
+
name: '계획',
|
|
152
|
+
en: 'Plan',
|
|
153
|
+
glyph: '☰',
|
|
154
|
+
hint: '먼저 계획 · 승인 뒤 실행',
|
|
155
|
+
tools: [...읽기, ...계획],
|
|
156
|
+
effort: 'deep',
|
|
157
|
+
think: 'high',
|
|
158
|
+
steps: 16,
|
|
159
|
+
say: [
|
|
160
|
+
"지금 하는 일은 **계획 세우기**다. 파일을 바꾸는 도구는 주어지지 않았다.",
|
|
161
|
+
"코드를 고치려 들지 마라. 계획을 내고 멈춘다.",
|
|
162
|
+
"",
|
|
163
|
+
"먼저 확인하라 — 지금 무엇이 어떻게 되어 있는지 모르면 계획이 아니라 희망이다.",
|
|
164
|
+
" Glob/Grep 으로 관련 파일을 찾고, Read 로 실제로 읽어라.",
|
|
165
|
+
"",
|
|
166
|
+
"그 다음 이 차례로 적어라.",
|
|
167
|
+
" 1. 목표 — 무엇이 끝나면 다 된 것인가 (확인할 수 있는 문장으로)",
|
|
168
|
+
" 2. 지금 상태 — 관련된 파일과 지금 동작 (경로:줄 로 짚어라)",
|
|
169
|
+
" 3. 바꿀 것 — 파일별로 무엇을 왜",
|
|
170
|
+
" 4. 순서 — 단계별로. 각 단계는 따로 확인할 수 있는 크기로 쪼개라",
|
|
171
|
+
" 5. 위험 — 무엇이 깨질 수 있고, 깨지면 어떻게 되돌리는가",
|
|
172
|
+
" 6. 확인 방법 — 무엇을 돌려 보면 됐는지 알 수 있는가",
|
|
173
|
+
"",
|
|
174
|
+
"단계는 TodoWrite 로도 적어라. 승인 뒤 그대로 이어서 하게 된다.",
|
|
175
|
+
"모르는 것이 있으면 지어내지 말고 \"이건 확인이 필요하다\" 고 적어라.",
|
|
176
|
+
"",
|
|
177
|
+
"마지막에 \"이대로 진행할까요?\" 로 끝내라. 승인을 받으면 /code 로 바꿔 실행한다.",
|
|
178
|
+
].join('\n'),
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
orchestrator: {
|
|
182
|
+
id: 'orchestrator',
|
|
183
|
+
name: '총괄',
|
|
184
|
+
en: 'Orchestrator',
|
|
185
|
+
glyph: '❋',
|
|
186
|
+
hint: '큰 일을 쪼개서 끝까지',
|
|
187
|
+
tools: [...읽기, ...계획, ...쓰기],
|
|
188
|
+
effort: 'save',
|
|
189
|
+
think: null,
|
|
190
|
+
steps: 40, // 여러 갈래를 끝까지 끌고 가야 한다
|
|
191
|
+
say: [
|
|
192
|
+
"지금 하는 일은 **큰 일을 끝까지 끌고 가기**다.",
|
|
193
|
+
"",
|
|
194
|
+
" 1. 시작하자마자 TodoWrite 로 전체를 단계로 쪼개 적어라. 머릿속에만 두지 마라.",
|
|
195
|
+
" 단계는 각각 따로 확인할 수 있는 크기여야 한다.",
|
|
196
|
+
" 2. 한 번에 하나만 진행 중으로 둬라. 끝나면 바로 표시하고 다음으로 넘어가라.",
|
|
197
|
+
" 3. 각 단계 끝에서 확인하라. 확인 없이 넘어가면 어디서 어긋났는지 못 찾는다.",
|
|
198
|
+
" 4. 막히면 멈추고 무엇에 막혔는지 보고하라. 우회로를 몰래 타지 마라.",
|
|
199
|
+
" 5. 계획이 틀린 것을 알게 되면 목록을 고쳐라. 틀린 계획을 끝까지 밀지 마라.",
|
|
200
|
+
"",
|
|
201
|
+
"다 끝나면 무엇을 했는지, 무엇을 못 했는지 같이 요약하라.",
|
|
202
|
+
"못 한 것을 빼고 요약하지 마라.",
|
|
203
|
+
].join('\n'),
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export const ORDER = ['auto', 'code', 'plan', 'architect', 'debug', 'ask', 'orchestrator'];
|
|
208
|
+
export const DEFAULT = 'auto';
|
|
209
|
+
|
|
210
|
+
/** 이름을 관대하게 받는다. 한글·영문·줄임말 다 통한다. */
|
|
211
|
+
export function normalize(v) {
|
|
212
|
+
const s = String(v ?? '').trim().toLowerCase();
|
|
213
|
+
if (!s) return null;
|
|
214
|
+
if (MODES[s]) return s;
|
|
215
|
+
const 별명 = {
|
|
216
|
+
'종합': 'auto', '자동': 'auto', '기본': 'auto', 'auto': 'auto', '맡김': 'auto',
|
|
217
|
+
'코드': 'code', 'c': 'code',
|
|
218
|
+
'계획': 'plan', '플랜': 'plan', 'p': 'plan',
|
|
219
|
+
'설계': 'architect', '아키': 'architect', 'arch': 'architect', 'a': 'architect',
|
|
220
|
+
'디버그': 'debug', '버그': 'debug', 'd': 'debug',
|
|
221
|
+
'묻기': 'ask', '질문': 'ask', '일상': 'ask', 'q': 'ask',
|
|
222
|
+
'총괄': 'orchestrator', '오케': 'orchestrator', 'orch': 'orchestrator', 'o': 'orchestrator',
|
|
223
|
+
};
|
|
224
|
+
return 별명[s] ?? null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function get(id) {
|
|
228
|
+
return MODES[normalize(id) ?? DEFAULT];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Shift+Tab 으로 돌릴 때 다음 모드. */
|
|
232
|
+
export function next(id) {
|
|
233
|
+
const i = ORDER.indexOf(normalize(id) ?? DEFAULT);
|
|
234
|
+
return ORDER[(i + 1) % ORDER.length];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** 이 모드가 파일을 바꿀 수 있나. 화면에 자물쇠를 그릴지 정하는 데 쓴다. */
|
|
238
|
+
export function canWrite(id) {
|
|
239
|
+
return get(id).tools.some((t) => 쓰기.includes(t));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 이 모드에서 모델에게 보여 줄 도구 이름들.
|
|
244
|
+
*
|
|
245
|
+
* 있는 것 중에서 고르는 것이지, 없는 것을 만들어 주지 않는다.
|
|
246
|
+
* 스킬이 없으면 Skill 은 애초에 없고, 오프라인이면 WebFetch 가 없다.
|
|
247
|
+
* 그 판단은 부르는 쪽이 이미 했다.
|
|
248
|
+
*/
|
|
249
|
+
export function allow(id, 있는것) {
|
|
250
|
+
const 허용 = new Set(get(id).tools);
|
|
251
|
+
return 있는것.filter((name) => 허용.has(name));
|
|
252
|
+
}
|