deel-local-cli 1.1.0 → 1.2.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 +347 -9
- package/README.md +317 -6
- package/package.json +2 -2
- package/src/agent/budget.js +0 -14
- package/src/agent/compact.js +87 -0
- package/src/agent/evolve.js +213 -0
- package/src/agent/loop.js +38 -1
- package/src/agent/modes.js +2 -0
- package/src/agent/route.js +55 -3
- package/src/agent/session.js +145 -1
- package/src/agent/threads.js +154 -0
- package/src/backend/http.js +41 -4
- package/src/commands.js +268 -4
- package/src/config.js +10 -1
- package/src/oneshot.js +4 -0
- package/src/preview/serve.js +326 -0
- package/src/repl.js +210 -34
- package/src/safety/guard.js +37 -2
- package/src/safety/undo.js +1 -1
- package/src/setup.js +2 -2
- package/src/skills/builtin//352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md +64 -0
- package/src/skills/builtin//352/271/212/354/235/264/354/236/210/352/262/214-/353/247/214/353/223/244/352/270/260/SKILL.md +78 -0
- package/src/skills/builtin//353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md +65 -0
- package/src/skills/builtin//354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md +74 -0
- package/src/skills/builtin//354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md +59 -0
- package/src/skills/builtin//354/260/250/352/267/274/354/260/250/352/267/274-/353/224/224/353/262/204/352/271/205/SKILL.md +73 -0
- package/src/skills/builtin//354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md +66 -0
- package/src/skills/discover.js +18 -3
- package/src/tools/edit-match.js +16 -5
- package/src/tools/excel-com.js +1 -1
- package/src/tools/excel.js +1 -1
- package/src/tools/fsutil.js +0 -8
- package/src/tools/index.js +1 -1
- package/src/tools/todo.js +19 -3
- package/src/ui/ansi.js +6 -2
- package/src/ui/inputbox.js +37 -8
- package/src/ui/md.js +227 -0
- package/src/ui/motion.js +212 -0
- package/src/ui/prompt.js +1 -1
- package/src/ui/screen.js +1 -1
- package/src/ui/status.js +14 -1
- package/src/ui/working.js +0 -3
- package/src/ui/wrap.js +1 -8
package/src/repl.js
CHANGED
|
@@ -6,27 +6,31 @@ import { c, say as 바로쓰기, mark, clip } from './ui/ansi.js';
|
|
|
6
6
|
import { headerLines } from './ui/status.js';
|
|
7
7
|
import { 화면고르기 } from './ui/screen.js';
|
|
8
8
|
import { STAGES } from './agent/effort.js';
|
|
9
|
-
import { handle, COMMANDS } from './commands.js';
|
|
9
|
+
import { handle, COMMANDS, 미리보기끄기 } from './commands.js';
|
|
10
10
|
import { next as nextWork, get as getWork, canWrite } from './agent/modes.js';
|
|
11
11
|
import { route } from './agent/route.js';
|
|
12
12
|
import { run } from './agent/loop.js';
|
|
13
|
-
import { Session } from './agent/session.js';
|
|
13
|
+
import { Session, repairToolPairs } from './agent/session.js';
|
|
14
14
|
import { makeScope } from './safety/guard.js';
|
|
15
15
|
import { History } from './safety/undo.js';
|
|
16
16
|
import { Audit } from './safety/audit.js';
|
|
17
|
-
import { activeProfile, load, resolveKey, save as saveCfg } from './config.js';
|
|
17
|
+
import { activeProfile, load, resolveKey, save as saveCfg, homeDir } from './config.js';
|
|
18
18
|
import { discover } from './skills/discover.js';
|
|
19
|
-
import { allowEndpoint, setOffline, isOffline
|
|
19
|
+
import { allowEndpoint, setOffline, isOffline } from './safety/network.js';
|
|
20
20
|
import { Store, latest, prune } from './agent/store.js';
|
|
21
|
+
import { Threads } from './agent/threads.js';
|
|
22
|
+
import { 배움 } from './agent/evolve.js';
|
|
23
|
+
import { 마크다운 } from './ui/md.js';
|
|
21
24
|
import { askHidden } from './ui/prompt.js';
|
|
22
25
|
import { explain, shows as levelShows } from './ui/level.js';
|
|
23
26
|
import { 고르기 as 승인고르기, 다음 as 승인다음 } from './ui/approve.js';
|
|
24
27
|
import { 추천, 채울글 } from './ui/complete.js';
|
|
28
|
+
import { 접어쓰기 } from './ui/wrap.js';
|
|
25
29
|
import { probeCtx, 기본값 as CTX_DEFAULT } from './backend/ctxsize.js';
|
|
26
30
|
import { renderDiff, shortStat } from './ui/diff.js';
|
|
27
31
|
import { expand as expandMentions } from './agent/mention.js';
|
|
28
32
|
import { 다붙이기 } from './backend/mcp.js';
|
|
29
|
-
import {
|
|
33
|
+
import { 읽기 as 기억읽기 } from './agent/memory.js';
|
|
30
34
|
import { 갈래고르기 } from './ui/working.js';
|
|
31
35
|
import { 모두끝내기 as 일감모두끝내기, 일감인자 } from './tools/jobs.js';
|
|
32
36
|
|
|
@@ -178,11 +182,19 @@ export async function chatLoop(opts = {}) {
|
|
|
178
182
|
say(` ${c.gray('이어할 대화가 없습니다. 새로 시작합니다.')}`);
|
|
179
183
|
} else {
|
|
180
184
|
store = new Store(root, target);
|
|
181
|
-
const { messages } = store.load();
|
|
185
|
+
const { messages: 적힌것 } = store.load();
|
|
186
|
+
/*
|
|
187
|
+
* 도구가 도는 중에 죽었으면 호출만 적히고 결과가 없다. 그대로 보내면
|
|
188
|
+
* 규격 서버가 400 을 내서 이어받자마자 첫 마디에서 죽는다. 손봐서 받는다.
|
|
189
|
+
*/
|
|
190
|
+
const { messages, 고친것 } = repairToolPairs(적힌것);
|
|
182
191
|
if (messages.length) {
|
|
183
192
|
session.messages = messages;
|
|
184
193
|
say('');
|
|
185
194
|
say(` ${mark.ok} ${c.bold(target)} ${c.gray(`— 메시지 ${messages.length}개를 이어 받았습니다.`)}`);
|
|
195
|
+
if (고친것) {
|
|
196
|
+
say(` ${c.gray(`중단된 도구 호출 ${고친것}개를 걷어냈습니다 — 그때 하던 일은 다시 시켜 주세요.`)}`);
|
|
197
|
+
}
|
|
186
198
|
}
|
|
187
199
|
}
|
|
188
200
|
}
|
|
@@ -505,6 +517,26 @@ export async function chatLoop(opts = {}) {
|
|
|
505
517
|
},
|
|
506
518
|
};
|
|
507
519
|
|
|
520
|
+
/*
|
|
521
|
+
* 겪어 본 것 (agent/evolve.js). 쓸수록 이 PC 에 맞춰 나아지는 자리다.
|
|
522
|
+
*
|
|
523
|
+
* 켤 때 두 가지를 받아 온다.
|
|
524
|
+
* · 프롬프트에 실을 몇 줄 — 여기서 되는 명령, 이 모델의 버릇
|
|
525
|
+
* · 지난번에 알아낸 토큰 배수 — 첫 턴부터 제대로 셈한다
|
|
526
|
+
*
|
|
527
|
+
* 둘 다 없으면 아무 일도 안 일어난다. 처음 켠 PC 는 지금과 똑같이 돈다.
|
|
528
|
+
*/
|
|
529
|
+
ctx.배움 = new 배움(root, homeDir());
|
|
530
|
+
session.배움요약 = ctx.배움.요약(conn.model);
|
|
531
|
+
const 아는배수 = ctx.배움.아는보정(conn.model);
|
|
532
|
+
if (아는배수) { session.보정 = 아는배수; session.보정잰것 = 1; }
|
|
533
|
+
|
|
534
|
+
/*
|
|
535
|
+
* 대화 갈래. 연결·도구·되돌리기는 같이 쓰고 오간 말만 여러 벌 갖는다.
|
|
536
|
+
* 갈래마다 저장 파일을 따로 열어서, 나중에 `/sessions` 로 각각 찾아갈 수 있다.
|
|
537
|
+
*/
|
|
538
|
+
ctx.갈래 = new Threads(session, ctx, () => new Store(root).begin({ model: conn.model, base: conn.base, root }), store);
|
|
539
|
+
|
|
508
540
|
// ── 컨텍스트 길이를 모델에서 긁어온다 ─────────────────────────────────
|
|
509
541
|
//
|
|
510
542
|
// 켤 때마다 서버에 물어본다. 저장된 값을 그대로 믿지 않는다 —
|
|
@@ -625,21 +657,63 @@ export async function chatLoop(opts = {}) {
|
|
|
625
657
|
prompt();
|
|
626
658
|
});
|
|
627
659
|
|
|
660
|
+
/*
|
|
661
|
+
* 계획을 승인받은 뒤 **사람이 다시 치지 않아도** 이어서 할 말.
|
|
662
|
+
*
|
|
663
|
+
* 전에는 계획 모드가 모델에게 "승인을 받으면 /code 로 바꿔 실행한다" 고
|
|
664
|
+
* 시켜 놓고, 정작 승인받는 자리도 이어가는 길도 없었다. 사람이 /code 를
|
|
665
|
+
* 알아서 쳐야 이어졌다 — 모드 설명에는 '승인 뒤 실행' 이라고 적혀 있었는데.
|
|
666
|
+
* 코드가 안 하는 것을 화면이 약속하고 있었던 셈이다.
|
|
667
|
+
*/
|
|
668
|
+
let 이어갈것 = null; // 이어서 보낼 말 (null 이면 사람 입력을 기다린다)
|
|
669
|
+
let 이어갈모드 = null; // 그때 쓸 작업 모드. 다시 고르지 않는다.
|
|
670
|
+
|
|
671
|
+
/*
|
|
672
|
+
* 계획을 한 눈에 보여 준다.
|
|
673
|
+
*
|
|
674
|
+
* 오른쪽 테두리를 안 그린다. 한글은 두 칸을 먹는데 이모지·기호는 아니어서
|
|
675
|
+
* 폭을 맞추려다 어긋나면 상자가 깨져 보인다 — 안 그리면 어긋날 것이 없다.
|
|
676
|
+
*/
|
|
677
|
+
const 계획상자 = (할일) => {
|
|
678
|
+
const 폭 = Math.max(40, Math.min(88, (process.stdout.columns || 80) - 6));
|
|
679
|
+
say(` ${c.hcyan('┌')} ${c.bold('계획')}`);
|
|
680
|
+
if (!할일.length) {
|
|
681
|
+
// TodoWrite 를 안 쓴 계획도 있다. 그때 빈 상자를 그리면 계획이 없는 줄 안다.
|
|
682
|
+
say(` ${c.hcyan('│')} ${c.gray('단계가 따로 적히지 않았습니다 — 위에 적힌 계획을 봐 주세요.')}`);
|
|
683
|
+
} else {
|
|
684
|
+
할일.forEach((t, i) => {
|
|
685
|
+
const 줄들 = 접어쓰기(String(t.text ?? ''), 폭 - 6);
|
|
686
|
+
줄들.forEach((줄, j) => {
|
|
687
|
+
const 앞 = j === 0 ? c.gray(String(i + 1).padStart(2) + '.') : ' ';
|
|
688
|
+
say(` ${c.hcyan('│')} ${앞} ${c.white(줄)}`);
|
|
689
|
+
});
|
|
690
|
+
});
|
|
691
|
+
}
|
|
692
|
+
say(` ${c.hcyan('└')} ${c.gray(`${할일.length}단계 · ${session.root}`)}`);
|
|
693
|
+
};
|
|
694
|
+
|
|
628
695
|
for (;;) {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
696
|
+
let text;
|
|
697
|
+
if (이어갈것 !== null) {
|
|
698
|
+
// 승인받아 이어가는 자리. 사람이 친 것이 아니므로 ❯ 로 찍지 않는다.
|
|
699
|
+
text = 이어갈것;
|
|
700
|
+
이어갈것 = null;
|
|
701
|
+
} else {
|
|
702
|
+
prompt();
|
|
703
|
+
입력기다림 = true;
|
|
704
|
+
// 줄이 이미 쌓여 있으면 그건 **일하는 동안 미리 쳐 둔 것**이다.
|
|
705
|
+
// 그때는 대화에 안 찍었으니(찍으면 이미 보낸 것처럼 보인다) 지금 찍는다.
|
|
706
|
+
const 예약이었나 = queue.length > 0;
|
|
707
|
+
const line = await nextLine();
|
|
708
|
+
입력기다림 = false;
|
|
709
|
+
if (line === null) break; // 입력이 끝났다 (파이프 종료 / Ctrl+D)
|
|
710
|
+
interrupted = false;
|
|
711
|
+
text = line.trim();
|
|
712
|
+
if (!text) continue;
|
|
713
|
+
if (예약이었나 && 상자쓰나) {
|
|
714
|
+
화면.입력지움();
|
|
715
|
+
say(` ${c.hcyan('❯')} ${c.white(text)} ${c.gray('(미리 쳐 둔 것)')}`);
|
|
716
|
+
}
|
|
643
717
|
}
|
|
644
718
|
|
|
645
719
|
const cmd = await handle(text, session, ctx);
|
|
@@ -672,16 +746,31 @@ export async function chatLoop(opts = {}) {
|
|
|
672
746
|
// 기본 모드는 안 건드린다 — 다음 한마디는 다시 처음부터 고른다.
|
|
673
747
|
// 사용자가 직접 고른 모드가 있으면 여기 안 들어온다. 사람이 고른 것을 뒤집지 않는다.
|
|
674
748
|
session.routed = null;
|
|
675
|
-
|
|
749
|
+
// 계획을 내고 승인을 받아야 하는 턴인가. 턴이 끝난 뒤 승인 창을 띄운다.
|
|
750
|
+
let 계획승인받나 = false;
|
|
751
|
+
if (이어갈모드) {
|
|
752
|
+
// 방금 승인받은 계획을 그대로 잇는 자리. 여기서 다시 고르면 안 된다 —
|
|
753
|
+
// "위 계획대로 진행해라" 에는 '계획' 이 들어 있어서 또 계획 모드로 간다.
|
|
754
|
+
session.routed = 이어갈모드;
|
|
755
|
+
이어갈모드 = null;
|
|
756
|
+
} else if (session.work === 'auto') {
|
|
676
757
|
const 골라진 = route(toSend);
|
|
677
758
|
if (골라진.mode) {
|
|
678
759
|
session.routed = 골라진.mode;
|
|
760
|
+
계획승인받나 = 골라진.겹침 === true;
|
|
679
761
|
const w = getWork(골라진.mode);
|
|
680
762
|
say('');
|
|
681
763
|
say(` ${c.hcyan(w.glyph)} ${c.bold(w.name)} ${c.gray('(' + w.en + ')')}`
|
|
682
764
|
+ ` ${c.gray('말 속에 ' + 골라진.why + ' 가 있어서')}`
|
|
683
765
|
+ (canWrite(골라진.mode) ? '' : ` ${c.green('· 파일은 안 바꿉니다')}`));
|
|
684
|
-
|
|
766
|
+
// 겹친 요청은 '파일을 안 바꾼다' 로 끝나면 안 된다. 시킨 일의 절반만 한 것이다.
|
|
767
|
+
// 그래서 여기서 뒷 절반이 온다는 것을 미리 말해 준다.
|
|
768
|
+
if (계획승인받나) {
|
|
769
|
+
say(` ${c.gray('계획과 실행이 같이 있어')} ${c.white('계획부터')} ${c.gray('냅니다.')}`
|
|
770
|
+
+ ` ${c.gray('보시고 승인하면')} ${c.white('그대로 이어서')} ${c.gray('합니다.')}`);
|
|
771
|
+
} else {
|
|
772
|
+
say(` ${c.gray('다르면')} ${c.cyan('/code')} ${c.gray('처럼 직접 고르세요. 그때부터는 안 바뀝니다.')}`);
|
|
773
|
+
}
|
|
685
774
|
}
|
|
686
775
|
}
|
|
687
776
|
|
|
@@ -692,12 +781,27 @@ export async function chatLoop(opts = {}) {
|
|
|
692
781
|
// 어디까지 적었는지. 도중에 죽어도 여기까지는 남아 있게 자주 흘려 보낸다.
|
|
693
782
|
let saved = session.messages.length;
|
|
694
783
|
const flush = () => {
|
|
695
|
-
for (const m of session.messages.slice(saved)) store.append(m);
|
|
784
|
+
for (const m of session.messages.slice(saved)) ctx.갈래.현재store().append(m);
|
|
696
785
|
saved = session.messages.length;
|
|
697
786
|
};
|
|
698
787
|
let tools = 0;
|
|
788
|
+
// 이 턴이 탈 없이 끝났나. 끊겼거나 터진 뒤에 승인 창을 띄우면 안 된다 —
|
|
789
|
+
// 계획이 반만 나온 것을 두고 "이대로 진행할까요?" 를 묻는 꼴이 된다.
|
|
790
|
+
let 턴탈났나 = false;
|
|
699
791
|
let thinkChars = 0;
|
|
700
792
|
let streamed = false;
|
|
793
|
+
/*
|
|
794
|
+
* 답을 그리는 자리. 턴마다 새로 만든다 — 앞 턴의 코드 울타리 상태가
|
|
795
|
+
* 다음 턴으로 새면 멀쩡한 답이 통째로 코드 블록으로 그려진다.
|
|
796
|
+
*/
|
|
797
|
+
const 답그림 = new 마크다운({ 폭: (process.stdout.columns || 80) - 6 });
|
|
798
|
+
// 답 흐름이 끊기는 자리마다 남은 반 줄을 비운다. 안 비우면 마지막 줄이 사라진다.
|
|
799
|
+
const 답비우기 = () => {
|
|
800
|
+
for (const 조각 of 답그림.끝()) {
|
|
801
|
+
if (typeof 조각 === 'string') 화면.붙임(` ${답표시} ${조각}\n`);
|
|
802
|
+
else 화면.붙임(조각.이어붙임 + (조각.끝났나 ? '\n' : ''));
|
|
803
|
+
}
|
|
804
|
+
};
|
|
701
805
|
let thinkingShown = false;
|
|
702
806
|
let stage = null;
|
|
703
807
|
// 접는 중 표시. 끝나거나 다른 글을 찍기 전에 반드시 멈춰야 한다.
|
|
@@ -769,15 +873,28 @@ export async function chatLoop(opts = {}) {
|
|
|
769
873
|
* 세로줄 하나면 된다. 칸을 나눠 그리는 화면으로 갈 이유가 없다 —
|
|
770
874
|
* 파이프로 넘기거나 기록으로 남길 때도 그대로 읽힌다.
|
|
771
875
|
*/
|
|
876
|
+
/*
|
|
877
|
+
* 답을 마크다운으로 그린다 (ui/md.js).
|
|
878
|
+
*
|
|
879
|
+
* 줄이 끝나야 그릴 수 있다 — `**굵` 까지 왔을 때는 그게 굵은 글씨가
|
|
880
|
+
* 될지 알 수 없고, 한 번 찍은 글자는 되돌릴 수 없다. 그래서 md 가
|
|
881
|
+
* 줄 단위로 모았다가 내놓는다. 줄이 화면 폭보다 길어지면 거기까지를
|
|
882
|
+
* 날것으로 흘려보낸다(md 가 알아서 한다) — 긴 문단에서 몇 초씩
|
|
883
|
+
* 아무것도 안 나오면 멈춘 것처럼 보이기 때문이다.
|
|
884
|
+
*/
|
|
772
885
|
case 'content':
|
|
773
886
|
clearThinking();
|
|
774
|
-
if (!streamed) { streamed = true; 화면.일바꿈('답');
|
|
775
|
-
|
|
887
|
+
if (!streamed) { streamed = true; 화면.일바꿈('답'); }
|
|
888
|
+
for (const 조각 of 답그림.넣기(ev.text)) {
|
|
889
|
+
if (typeof 조각 === 'string') { 화면.붙임(` ${답표시} ${조각}\n`); continue; }
|
|
890
|
+
// 아직 안 끝난 긴 줄. 그 줄이 처음 나가는 것이면 세로줄을 앞에 세운다.
|
|
891
|
+
화면.붙임((조각.첫조각 ? ` ${답표시} ` : '') + 조각.이어붙임 + (조각.끝났나 ? '\n' : ''));
|
|
892
|
+
}
|
|
776
893
|
break;
|
|
777
894
|
|
|
778
895
|
case 'tool_start':
|
|
779
896
|
clearThinking();
|
|
780
|
-
if (streamed) { say(''); streamed = false; }
|
|
897
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
781
898
|
// 문구를 지금 하는 일에 맞춘다. 아무 말이나 돌려 대면 두 번째부터
|
|
782
899
|
// 아무도 안 읽고, 그때부터는 화면이 조용한 것과 같아진다.
|
|
783
900
|
화면.일바꿈(갈래고르기(ev.name));
|
|
@@ -788,7 +905,7 @@ export async function chatLoop(opts = {}) {
|
|
|
788
905
|
// 여럿을 같이 돌린다 — 한 줄로 알리고, 이름은 결과와 붙여서 그린다.
|
|
789
906
|
case 'tools_start':
|
|
790
907
|
clearThinking();
|
|
791
|
-
if (streamed) { say(''); streamed = false; }
|
|
908
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
792
909
|
화면.일바꿈(갈래고르기(ev.names?.[0]));
|
|
793
910
|
say('');
|
|
794
911
|
say(` ${c.gray(`${ev.count}개를 함께 돌립니다`)} ${c.gray('·')} ${c.gray(ev.names.join(' '))}`);
|
|
@@ -800,7 +917,7 @@ export async function chatLoop(opts = {}) {
|
|
|
800
917
|
// '시작' 을 안 거쳤다. 그래서 '생각 중…' 줄이 안 지워진 채로 결과가
|
|
801
918
|
// 그 줄 뒤에 가서 붙고, 이름도 없이 "└ 인자가 잘렸습니다" 만 남는다.
|
|
802
919
|
// 무슨 도구가 왜 그랬는지 알 수 없는 화면이 된다.
|
|
803
|
-
if (ev.showLabel) { clearThinking(); if (streamed) { say(''); streamed = false; } say(''); }
|
|
920
|
+
if (ev.showLabel) { clearThinking(); if (streamed) { 답비우기(); say(''); streamed = false; } say(''); }
|
|
804
921
|
// 같이 돈 것은 이름을 다시 적어 준다. 안 그러면 어느 결과인지 모른다.
|
|
805
922
|
if (ev.parallel || ev.showLabel) say(` ${toolLabel(ev.name, ev.args)}`);
|
|
806
923
|
if (ev.name === 'TodoWrite' && ev.result?.todos) {
|
|
@@ -875,7 +992,7 @@ export async function chatLoop(opts = {}) {
|
|
|
875
992
|
*/
|
|
876
993
|
case 'task_start':
|
|
877
994
|
clearThinking();
|
|
878
|
-
if (streamed) { say(''); streamed = false; }
|
|
995
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
879
996
|
화면.일바꿈('하위', clip(ev.목적, 24));
|
|
880
997
|
say('');
|
|
881
998
|
say(` ${c.hmagenta('⌥')} ${c.bold('하위 작업')} ${c.white(clip(ev.목적, 60))}`
|
|
@@ -885,7 +1002,7 @@ export async function chatLoop(opts = {}) {
|
|
|
885
1002
|
|
|
886
1003
|
case 'task_done': {
|
|
887
1004
|
clearThinking();
|
|
888
|
-
if (streamed) { say(''); streamed = false; }
|
|
1005
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
889
1006
|
const 끝 = ev.끝 ?? {};
|
|
890
1007
|
const 잘됨 = 끝.type === 'done';
|
|
891
1008
|
const 왜 = { done: '끝냈습니다', limit: '걸음 수를 다 써서 멈췄습니다 — 다 못 했습니다',
|
|
@@ -920,6 +1037,16 @@ export async function chatLoop(opts = {}) {
|
|
|
920
1037
|
접는중 = true;
|
|
921
1038
|
break;
|
|
922
1039
|
|
|
1040
|
+
/*
|
|
1041
|
+
* 요약 압축보다 먼저 오는 것. 대화는 안 건드리고 옛 도구 결과만 접었다.
|
|
1042
|
+
* 조용히 하면 사람은 어느 파일 내용이 왜 사라졌는지 모른다 — 한 줄로 알린다.
|
|
1043
|
+
*/
|
|
1044
|
+
case 'folded':
|
|
1045
|
+
clearThinking();
|
|
1046
|
+
say(` ${c.cyan('◲')} ${c.gray(`오래된 도구 결과 ${ev.접은것}개를 접었습니다 — `)}`
|
|
1047
|
+
+ `${c.white(ev.아낀토큰.toLocaleString())} ${c.gray('토큰을 비웠습니다. 대화는 그대로입니다.')}`);
|
|
1048
|
+
break;
|
|
1049
|
+
|
|
923
1050
|
case 'compacted': {
|
|
924
1051
|
접기멈춤();
|
|
925
1052
|
const 줄인 = ev.before - ev.after;
|
|
@@ -928,7 +1055,7 @@ export async function chatLoop(opts = {}) {
|
|
|
928
1055
|
`${c.green(`(${Math.round((줄인 / Math.max(1, ev.before)) * 100)}% 줄어듦)`)}`);
|
|
929
1056
|
if (ev.fallback) say(` ${c.yellow('요약을 못 받아 그냥 줄였습니다.')}`);
|
|
930
1057
|
// 접히면 이력이 통째로 바뀐다. 덧붙이기로는 못 맞추니 새로 적는다.
|
|
931
|
-
store.replace(session.messages, `압축 — ${ev.folded}개를 요약으로`);
|
|
1058
|
+
ctx.갈래.현재store().replace(session.messages, `압축 — ${ev.folded}개를 요약으로`);
|
|
932
1059
|
saved = session.messages.length;
|
|
933
1060
|
break;
|
|
934
1061
|
}
|
|
@@ -970,8 +1097,9 @@ export async function chatLoop(opts = {}) {
|
|
|
970
1097
|
|
|
971
1098
|
// 같은 자리를 계속 반복하고 있다. 두면 컨텍스트만 차고 아무것도 안 나온다.
|
|
972
1099
|
case 'stuck':
|
|
1100
|
+
턴탈났나 = true;
|
|
973
1101
|
clearThinking();
|
|
974
|
-
if (streamed) { say(''); streamed = false; }
|
|
1102
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
975
1103
|
say('');
|
|
976
1104
|
say(` ${c.yellow('⊘')} ${c.bold('같은 자리에서 헛돌고 있어 멈췄습니다.')}`);
|
|
977
1105
|
say(` ${c.gray(ev.why)}`);
|
|
@@ -991,8 +1119,9 @@ export async function chatLoop(opts = {}) {
|
|
|
991
1119
|
break;
|
|
992
1120
|
|
|
993
1121
|
case 'aborted':
|
|
1122
|
+
턴탈났나 = true;
|
|
994
1123
|
clearThinking();
|
|
995
|
-
if (streamed) { say(''); streamed = false; }
|
|
1124
|
+
if (streamed) { 답비우기(); say(''); streamed = false; }
|
|
996
1125
|
say('');
|
|
997
1126
|
// 남았는지 아닌지를 **사실대로** 말한다. 전에는 무조건 '남아 있다' 고 했는데
|
|
998
1127
|
// 실제로는 아무것도 안 남는 경우가 있었다. 그러면 "이어서 해줘" 라고 했을 때
|
|
@@ -1003,6 +1132,7 @@ export async function chatLoop(opts = {}) {
|
|
|
1003
1132
|
break;
|
|
1004
1133
|
|
|
1005
1134
|
case 'error':
|
|
1135
|
+
턴탈났나 = true;
|
|
1006
1136
|
clearThinking();
|
|
1007
1137
|
say('');
|
|
1008
1138
|
오류보이기(ev.text);
|
|
@@ -1010,12 +1140,13 @@ export async function chatLoop(opts = {}) {
|
|
|
1010
1140
|
|
|
1011
1141
|
case 'done':
|
|
1012
1142
|
clearThinking();
|
|
1013
|
-
if (streamed) say('');
|
|
1143
|
+
if (streamed) { 답비우기(); say(''); }
|
|
1014
1144
|
만든파일보이기(ev.files);
|
|
1015
1145
|
break;
|
|
1016
1146
|
}
|
|
1017
1147
|
}
|
|
1018
1148
|
} catch (err) {
|
|
1149
|
+
턴탈났나 = true;
|
|
1019
1150
|
접기멈춤();
|
|
1020
1151
|
clearThinking();
|
|
1021
1152
|
say('');
|
|
@@ -1038,6 +1169,45 @@ export async function chatLoop(opts = {}) {
|
|
|
1038
1169
|
if (dIn || dOut) bits.push(`↑${dIn.toLocaleString()} ↓${dOut.toLocaleString()}`);
|
|
1039
1170
|
say('');
|
|
1040
1171
|
say(` ${c.gray('─'.repeat(2))} ${c.gray(bits.join(c.gray(' · ')))}`);
|
|
1172
|
+
|
|
1173
|
+
/*
|
|
1174
|
+
* ── 계획 승인 ──────────────────────────────────────────────────────
|
|
1175
|
+
*
|
|
1176
|
+
* 겹친 요청("계획해주고 만들어줘")일 때만 온다. 그냥 "고쳐줘" 에는 안 뜬다 —
|
|
1177
|
+
* 이 프로젝트는 승인 게이트 대신 되돌리기로 가기로 했고, 그 결정을
|
|
1178
|
+
* 아무 때나 뜨는 창으로 갉아먹으면 안 된다.
|
|
1179
|
+
*
|
|
1180
|
+
* ⏎ 하나로 진행되는 것이 중요하다. 여기서 뭘 더 치게 하면 사람은
|
|
1181
|
+
* 애초에 계획 같은 걸 안 보려 든다.
|
|
1182
|
+
*/
|
|
1183
|
+
if (계획승인받나 && !턴탈났나) {
|
|
1184
|
+
const 할일 = (ctx.todos ?? []).filter((t) => t.state !== 'done');
|
|
1185
|
+
say('');
|
|
1186
|
+
계획상자(할일);
|
|
1187
|
+
say('');
|
|
1188
|
+
const 답 = String(await ask(
|
|
1189
|
+
`이대로 진행할까요? ${c.gray('⏎ 진행 · n 취소 · 그 밖엔 고칠 점')}`,
|
|
1190
|
+
{ def: 'y' },
|
|
1191
|
+
)).trim();
|
|
1192
|
+
const 낮춘 = 답.toLowerCase();
|
|
1193
|
+
|
|
1194
|
+
if (['n', 'no', 'ㄴ', '취소', '아니', '아니요', '아니오'].includes(낮춘)) {
|
|
1195
|
+
say(` ${c.gray('그만뒀습니다. 계획은 위에 남아 있으니 이어서 말씀하셔도 됩니다.')}`);
|
|
1196
|
+
} else if (['y', 'yes', 'ㅇ', 'ㅇㅇ', 'ㄱ', 'ㄱㄱ', '네', '응', '진행', 'ok'].includes(낮춘)) {
|
|
1197
|
+
// 계획을 다시 적으라고 하면 두 번 적는다. 컨텍스트만 먹고 사람은 같은 글을 두 번 본다.
|
|
1198
|
+
이어갈것 = '위 계획을 승인받았다. 계획을 다시 적지 말고 지금부터 그대로 실행해라.'
|
|
1199
|
+
+ ' 적어 둔 단계를 하나씩 끝내고, 다 끝나면 무엇을 만들었는지만 짧게 알려라.';
|
|
1200
|
+
이어갈모드 = 'code';
|
|
1201
|
+
say('');
|
|
1202
|
+
say(` ${c.hgreen('▶')} ${c.gray('계획대로 진행합니다.')}`);
|
|
1203
|
+
} else {
|
|
1204
|
+
// 'e' 를 따로 두지 않는다 — 고칠 점을 바로 치는 것이 한 걸음 짧다.
|
|
1205
|
+
이어갈것 = `계획에서 이걸 고쳐서 **계획만** 다시 내라 (아직 실행하지 마라): ${답}`;
|
|
1206
|
+
이어갈모드 = 'plan';
|
|
1207
|
+
say('');
|
|
1208
|
+
say(` ${c.hcyan('☰')} ${c.gray('그 점을 반영해 계획을 다시 냅니다.')}`);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1041
1211
|
}
|
|
1042
1212
|
|
|
1043
1213
|
rl.close();
|
|
@@ -1051,6 +1221,12 @@ export async function chatLoop(opts = {}) {
|
|
|
1051
1221
|
const 껐다 = 일감모두끝내기();
|
|
1052
1222
|
if (껐다) say(` ${mark.ok} ${c.gray(`뒤에서 돌던 명령 ${껐다}개를 같이 껐습니다.`)}`);
|
|
1053
1223
|
}
|
|
1224
|
+
// 미리보기 서버도 같은 이유로 거둔다. 안 거두면 포트를 물고 있는 채로 남아,
|
|
1225
|
+
// 다음에 띄운 것과 두 개가 뜬다 — 어느 쪽을 보고 있는지 알 수 없게 된다.
|
|
1226
|
+
{
|
|
1227
|
+
const 껐다 = await 미리보기끄기();
|
|
1228
|
+
if (껐다) say(` ${mark.ok} ${c.gray(`미리보기를 껐습니다 (${껐다.서버.url}).`)}`);
|
|
1229
|
+
}
|
|
1054
1230
|
// 끝맺음은 화면을 접기 **전에** 그린다. close() 가 상자를 걷어내므로,
|
|
1055
1231
|
// 그 뒤에 찍으면 걷어낸 자리에 뜬금없이 한 줄이 남는다.
|
|
1056
1232
|
say('');
|
package/src/safety/guard.js
CHANGED
|
@@ -72,12 +72,47 @@ const BLOCKED = [
|
|
|
72
72
|
{ re: /\brm\s+(-[a-z]*[rR][a-z]*f|-[a-z]*f[a-z]*[rR])\b[^|;&]*\s(\/|~|\$HOME)\s*$/i, why: '뿌리 폴더를 통째로 지우려 합니다' },
|
|
73
73
|
{ re: /\b(mkfs|fdisk|diskpart)\b/i, why: '디스크를 초기화하는 명령입니다' },
|
|
74
74
|
{ re: /\bformat\s+[a-z]:/i, why: '드라이브를 포맷하는 명령입니다' },
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
// 윈도우 짝을 빠뜨리면 안 된다 — deel 은 윈도우에서 주로 돈다.
|
|
76
|
+
//
|
|
77
|
+
// 옛 규칙은 스위치가 **차례대로** 오고 끝에 `\*` 가 붙은 모양만 봤다.
|
|
78
|
+
// 그래서 `del /f /s /q C:\` 와 `rd /q /s C:\` 가 그대로 새어 나갔다.
|
|
79
|
+
// 스위치는 아무 차례로나 오므로 차례를 안 따진다.
|
|
80
|
+
{ re: /\b(rd|rmdir)\b[^|;&]*\s\/s\b/i, why: '폴더를 통째로 지웁니다 — 정션이 있으면 원본까지 딸려 갑니다' },
|
|
81
|
+
{ re: /\bdel\b[^|;&]*\s\/[sq]\b[^|;&]*(\\\*|\s[a-z]:\\?\s*$)/i, why: '하위 폴더까지 전부 지웁니다' },
|
|
82
|
+
// 파워셸도 같은 일을 한다. 드라이브 뿌리나 집 폴더를 통째로 미는 것만 본다 —
|
|
83
|
+
// 폴더 하나 지우는 평범한 Remove-Item 까지 막으면 도구를 못 쓴다.
|
|
84
|
+
{
|
|
85
|
+
re: /Remove-Item\b[^|;&]*\s-(Recurse|r)\b[^|;&]*\s(-?[a-z]*\s*)*([a-z]:\\?|~|\$HOME|\$env:USERPROFILE)\s*$/i,
|
|
86
|
+
why: '드라이브나 집 폴더를 통째로 지웁니다',
|
|
87
|
+
},
|
|
77
88
|
{ re: /git\s+push\b[^|;&]*--force(?!-with-lease)/i, why: '원격 이력을 덮어씁니다 (--force-with-lease 를 쓰세요)' },
|
|
78
89
|
// 낱말만 보면 안 된다. `node scripts/shutdown.js` 나 `npm run reboot` 이
|
|
79
90
|
// '시스템을 끕니다' 로 막히고 감사기록에까지 남았다. 명령의 **첫 낱말**일 때만 본다.
|
|
80
91
|
{ re: /(^|[|;&]\s*)(sudo\s+)?(shutdown|reboot|halt|poweroff)\b/i, why: '시스템을 끕니다' },
|
|
92
|
+
// 포크 폭탄.
|
|
93
|
+
//
|
|
94
|
+
// 껐다 켜면 기계는 돌아오니 '되돌릴 수 없는 것' 은 아니다. 그런데 되돌아오지
|
|
95
|
+
// 않는 것이 하나 있다 — **아직 안 적은 것들.** 기계가 굳으면 열어 둔 것을
|
|
96
|
+
// 저장할 수도, 하던 대화를 끝낼 수도 없다. 그래서 목록에 넣는다.
|
|
97
|
+
//
|
|
98
|
+
// 이름은 아무거나 될 수 있으니(`:` 든 `bomb` 든) 이름 자체를 보지 않고 **모양**을
|
|
99
|
+
// 본다: 함수를 만들고, 그 안에서 **저를 불러 곧바로 파이프나 뒤로 보내기에**
|
|
100
|
+
// 물리고, 밖에서 그 이름을 부른다. 뒷갈이(\2)로 같은 이름인지까지 본다.
|
|
101
|
+
//
|
|
102
|
+
// '저를 부른 **바로 뒤**가 | 나 &' 인 것이 열쇠다. 그냥 '이름이 몸 안에 있고
|
|
103
|
+
// & 도 있다' 로 보면 `up(){ docker compose up -d & }; up` 같은 평범한 함수가
|
|
104
|
+
// 걸린다 — 거기서는 up 뒤에 `-d` 가 온다.
|
|
105
|
+
//
|
|
106
|
+
// 이름 앞뒤로 낱말 경계도 본다. 그래야 `tee -a out.log` 의 log 나
|
|
107
|
+
// `deploy_app` 의 deploy 를 저를 부르는 것으로 잘못 읽지 않는다.
|
|
108
|
+
// 앞자리에 따옴표와 여는 괄호도 넣는다. 안 넣으면 `bash -c ":(){ :|:& };:"` 처럼
|
|
109
|
+
// 한 겹 싸는 것만으로 그대로 새어 나간다.
|
|
110
|
+
{
|
|
111
|
+
re: /(^|[|;&\s"'(])([\w.:]{1,16})\s*\(\s*\)\s*\{[^}]*(?<![\w.])\2(?![\w.])\s*[|&][^}]*\}\s*;?\s*(?<![\w.])\2(?![\w.])/,
|
|
112
|
+
why: '스스로를 끝없이 불려 기계를 멈춰 세웁니다 (포크 폭탄)',
|
|
113
|
+
},
|
|
114
|
+
// 같은 것을 한 줄로 적는 오래된 방법. 이 모양으로 쓸 다른 일이 없다.
|
|
115
|
+
{ re: /\bfork\s+while\s+fork\b/i, why: '스스로를 끝없이 불려 기계를 멈춰 세웁니다 (포크 폭탄)' },
|
|
81
116
|
{ re: /curl[^|]*\|\s*(ba)?sh/i, why: '받은 스크립트를 그대로 실행합니다' },
|
|
82
117
|
{ re: /\biwr\b[^|]*\|\s*iex\b/i, why: '받은 스크립트를 그대로 실행합니다' },
|
|
83
118
|
];
|
package/src/safety/undo.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 되돌리기. 승인 프롬프트를 안 쓰는 대신 이게 안전망이다.
|
|
2
2
|
// 파일을 고치기 전에 항상 이전 내용을 떠 놓고, /undo 로 턴 단위로 되돌린다.
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, appendFileSync,
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, appendFileSync, statSync } from 'node:fs';
|
|
5
5
|
import { looksBinary } from '../tools/encoding.js';
|
|
6
6
|
|
|
7
7
|
// 되돌리기 이력은 파일 내용을 통째로 담는다. 이만큼 커지면 오래된 턴을 버린다.
|
package/src/setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// 첫 실행 마법사 + 진단 실행.
|
|
2
|
-
import { c, say, rule, mark
|
|
3
|
-
import { ask, pick
|
|
2
|
+
import { c, say, rule, mark } from './ui/ansi.js';
|
|
3
|
+
import { ask, pick } from './ui/prompt.js';
|
|
4
4
|
import { spin } from './ui/spinner.js';
|
|
5
5
|
import { detect } from './backend/detect.js';
|
|
6
6
|
import { probe } from './backend/probe.js';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 검사-먼저
|
|
3
|
+
description: 고치기 전에 실패하는 검사부터 만들기. 결함·새 기능에 쓴다
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 검사 먼저
|
|
7
|
+
|
|
8
|
+
## 왜 있나
|
|
9
|
+
|
|
10
|
+
"고쳤습니다" 는 증명이 아니다. 무엇이 어떻게 틀렸는지를 **먼저 못 박아
|
|
11
|
+
두지 않으면**, 고친 뒤에 통과하는 것이 원래부터 통과하던 것인지 알 수 없다.
|
|
12
|
+
|
|
13
|
+
실제로 겪는 순서는 이렇다. 검사를 안 만들고 고친다 → 통과한다 → 사실은
|
|
14
|
+
그 자리를 한 번도 안 밟는 검사였다 → 결함이 그대로 나간다.
|
|
15
|
+
|
|
16
|
+
## 순서
|
|
17
|
+
|
|
18
|
+
### 1. 빨강 — 실패하는 것을 먼저 만든다
|
|
19
|
+
|
|
20
|
+
- 지금 코드로 **반드시 실패해야 한다.** 통과하면 그 검사는 아무것도 안 잰다
|
|
21
|
+
- 실패하는 것을 눈으로 확인한다. 이 단계를 건너뛰면 아무 의미가 없다
|
|
22
|
+
- 이름은 무엇이 잘못됐는지 말해야 한다.
|
|
23
|
+
`'끝내기가 돌아왔으면 이미 죽어 있다'` 가 `'jobs test 3'` 보다 낫다
|
|
24
|
+
|
|
25
|
+
### 2. 초록 — 통과할 만큼만 고친다
|
|
26
|
+
|
|
27
|
+
- 통과에 필요한 것만. 김에 다른 것까지 손보지 않는다
|
|
28
|
+
- 통과하면 멈춘다
|
|
29
|
+
|
|
30
|
+
### 3. 다시 본다
|
|
31
|
+
|
|
32
|
+
- 전체 검사를 돌린다. 다른 것이 깨졌으면 아직 안 끝난 것이다
|
|
33
|
+
- 검사가 우연히 통과하는 것은 아닌지 본다 — **일부러 코드를 되돌려 보고
|
|
34
|
+
다시 실패하는지** 확인하면 확실하다
|
|
35
|
+
|
|
36
|
+
## 무엇을 재야 하나
|
|
37
|
+
|
|
38
|
+
**말이 아니라 결과물을 잰다.**
|
|
39
|
+
|
|
40
|
+
- ✗ 화면에 "만들었습니다" 가 떴다
|
|
41
|
+
- ✓ 파일이 실제로 생겼고 내용이 맞다
|
|
42
|
+
|
|
43
|
+
- ✗ 함수가 오류를 안 던졌다
|
|
44
|
+
- ✓ 프로세스가 정말 죽었다 (커 가던 표시 파일이 더 이상 안 자란다)
|
|
45
|
+
|
|
46
|
+
### 안 되어야 하는 것도 같은 무게로 잰다
|
|
47
|
+
|
|
48
|
+
되는 것만 재면 절반이다. 승인 창이 **떠야 할 때** 뜨는 것만 재고 **안 떠야
|
|
49
|
+
할 때** 안 뜨는 것을 안 재면, 아무 때나 뜨는 것을 못 잡는다.
|
|
50
|
+
|
|
51
|
+
## 검사가 못 미더울 때
|
|
52
|
+
|
|
53
|
+
- 검사가 바깥 도구(tar·unzip·python)에 기대면, 그게 없는 자리에서 조용히
|
|
54
|
+
건너뛰어진다. **건너뛴 것을 반드시 화면에 남긴다**
|
|
55
|
+
- 서버를 띄우는 검사는 **포트 0**(커널이 빈 포트를 준다)으로 띄운다.
|
|
56
|
+
고정 포트는 남의 것을 뺏는다
|
|
57
|
+
- 임시 폴더는 매번 새로 만들고 끝나면 지운다. 진짜 설정 폴더를 안 건드린다
|
|
58
|
+
|
|
59
|
+
## 하지 말 것
|
|
60
|
+
|
|
61
|
+
- 고친 뒤에 검사 만들기 — 그 검사는 십중팔구 그냥 통과한다
|
|
62
|
+
- 실패를 안 보고 넘어가기
|
|
63
|
+
- 검사를 통과시키려고 검사를 고치기 (원인을 봐야 한다)
|
|
64
|
+
- 재기 어렵다고 안 재기 — 못 재면 **못 잰다고 적어 둔다**
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 깊이있게-만들기
|
|
3
|
+
description: 만들라는 요청에 최소치만 내지 않기. 고도화·제대로·보고용·선포용에 쓴다
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 깊이있게 만들기
|
|
7
|
+
|
|
8
|
+
## 언제 쓰나
|
|
9
|
+
|
|
10
|
+
무엇을 **만들어** 달라는 요청인데, 다음 중 하나라도 걸리면 이걸 쓴다.
|
|
11
|
+
|
|
12
|
+
- "고도화", "제대로", "잘", "본격적으로", "정식으로"
|
|
13
|
+
- 보는 사람이 있다 — 선포용, 보고용, 발표용, 제출용, 대외용
|
|
14
|
+
- 만들 것의 개수를 안 정해 줬다 ("자료들", "내용들", "문서 묶음")
|
|
15
|
+
|
|
16
|
+
## 왜 있나
|
|
17
|
+
|
|
18
|
+
시킨 것의 **최소치**만 내고 끝내는 일이 잦다. "AX 비전 선포 자료 만들어줘"
|
|
19
|
+
에 파일 세 개를 만들고 멈추는 식이다. 문법으로는 틀린 게 없다. 그런데
|
|
20
|
+
그걸 받아서 실제로 선포를 할 수는 없다.
|
|
21
|
+
|
|
22
|
+
받는 사람이 **그걸 그대로 쓸 수 있는가**가 기준이다. 더 채워야 한다면
|
|
23
|
+
아직 안 끝난 것이다.
|
|
24
|
+
|
|
25
|
+
## 어떻게
|
|
26
|
+
|
|
27
|
+
### 1. 요청의 무게부터 잰다
|
|
28
|
+
|
|
29
|
+
무게는 낱말이 아니라 **쓰임**에서 나온다.
|
|
30
|
+
|
|
31
|
+
| 쓰임 | 무게 | 대략 |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| 나 혼자 보려고 | 가벼움 | 파일 1~2개, 뼈대만 |
|
|
34
|
+
| 팀 안에서 돌려 볼 것 | 보통 | 3~6개, 읽는 순서 + 각 문서가 자립 |
|
|
35
|
+
| 밖에 나가거나 위로 올라갈 것 | 무거움 | 6개 이상, 요약본 · 근거 · 실행안 · 반론 대비까지 |
|
|
36
|
+
|
|
37
|
+
애매하면 **한 단계 무겁게** 잡는다. 넘치는 것은 지우면 되지만, 모자라면
|
|
38
|
+
사람이 처음부터 다시 시켜야 한다.
|
|
39
|
+
|
|
40
|
+
### 2. 무엇이 빠졌는지 먼저 센다
|
|
41
|
+
|
|
42
|
+
만들기 전에 이 목록을 훑는다. 무거운 일일수록 아래쪽까지 내려간다.
|
|
43
|
+
|
|
44
|
+
- **한 장 요약** — 바쁜 사람이 이것만 봐도 되는가
|
|
45
|
+
- **왜 지금인가** — 배경·현황·안 하면 생기는 일
|
|
46
|
+
- **무엇을** — 목표를 확인 가능한 문장으로. "개선한다" 는 목표가 아니다
|
|
47
|
+
- **어떻게** — 단계와 시기. 단계마다 끝났는지 알 방법
|
|
48
|
+
- **누가** — 역할·책임. 이름 자리가 비어도 자리는 만들어 둔다
|
|
49
|
+
- **얼마나** — 사람·돈·기간. 모르면 "확인 필요" 라고 적는다
|
|
50
|
+
- **어떻게 잴 것인가** — 지표와 목표치
|
|
51
|
+
- **무엇이 걸리는가** — 위험·반대 의견과 그에 대한 답
|
|
52
|
+
- **읽는 순서** — 문서가 셋을 넘으면 README 가 있어야 한다
|
|
53
|
+
|
|
54
|
+
### 3. 각 문서가 혼자 서게 만든다
|
|
55
|
+
|
|
56
|
+
문서 하나를 떼어 내도 그것만으로 말이 되어야 한다. 제목, 한 줄 요약,
|
|
57
|
+
본문, 그리고 다음에 볼 것. **표와 목록으로 끝내지 않는다** — 표는 근거를
|
|
58
|
+
못 담는다. 표 밑에 왜 그런지 한두 줄을 붙인다.
|
|
59
|
+
|
|
60
|
+
### 4. 모르는 것은 지어내지 않는다
|
|
61
|
+
|
|
62
|
+
숫자·날짜·조직 이름·예산은 확인이 필요한 것들이다. 지어내면 그 문서는
|
|
63
|
+
쓸 수 없게 된다. `> 확인 필요: …` 로 눈에 띄게 남긴다. 빈 자리는 흠이
|
|
64
|
+
아니지만 틀린 값은 흠이다.
|
|
65
|
+
|
|
66
|
+
### 5. 끝내기 전에 되묻는다
|
|
67
|
+
|
|
68
|
+
- 받는 사람이 이걸 그대로 쓸 수 있나
|
|
69
|
+
- 표만 있고 근거가 없는 자리는 없나
|
|
70
|
+
- 지어낸 숫자는 없나
|
|
71
|
+
- 셋 이상인데 읽는 순서가 없지는 않나
|
|
72
|
+
|
|
73
|
+
## 하지 말 것
|
|
74
|
+
|
|
75
|
+
- 개수를 안 정해 줬다고 최소로 만들기
|
|
76
|
+
- 목차만 만들고 본문을 비워 두기
|
|
77
|
+
- 길이로 때우기 — 같은 말을 늘려 쓰는 것은 깊이가 아니다
|
|
78
|
+
- 사람이 준 자료를 안 읽고 일반론으로 채우기
|