deel-local-cli 1.13.1 → 1.15.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.ko.md +32 -26
- package/README.md +32 -26
- package/bin/deel.js +6 -1
- package/package.json +3 -2
- package/src/acp/serve.js +71 -8
- package/src/agent/compact.js +25 -2
- package/src/agent/filemem.js +17 -0
- package/src/agent/loop.js +28 -1
- package/src/agent/session.js +36 -1
- package/src/agent/threads.js +33 -0
- package/src/cmdnames.js +68 -0
- package/src/commands.js +49 -71
- package/src/lsp/servers.js +9 -3
- package/src/oneshot.js +61 -1
- package/src/pack/selfpack.js +15 -2
- package/src/pack/sheet.en.js +16 -2
- package/src/repl.js +1 -1
- package/src/safety/guard.js +74 -1
- package/src/safety/undo.js +37 -2
- package/src/tools/fsutil.js +57 -1
- package/src/tools/index.js +162 -28
- package/src/tools/lsp.js +1 -1
- package/src/tools/outline.js +2 -2
- package/src/tools/verify.js +2 -2
package/src/agent/threads.js
CHANGED
|
@@ -27,6 +27,8 @@
|
|
|
27
27
|
// 한 줄기로 읽어서, 서로 상관없는 대화가 한 덩어리가 된다.
|
|
28
28
|
|
|
29
29
|
/** 이름이 없을 때 붙여 줄 이름. 사람이 목록에서 알아볼 수 있으면 된다. */
|
|
30
|
+
import { 파일기억 } from './filemem.js';
|
|
31
|
+
|
|
30
32
|
function 기본이름(n) { return `갈래 ${n}`; }
|
|
31
33
|
|
|
32
34
|
export class Threads {
|
|
@@ -49,9 +51,20 @@ export class Threads {
|
|
|
49
51
|
* 돌아오는데 남은 할 일은 안 돌아왔다.
|
|
50
52
|
*/
|
|
51
53
|
첫store?.살림따라가기?.(session);
|
|
54
|
+
/*
|
|
55
|
+
* 파일기억·filesRead 도 **갈래 것**이다.
|
|
56
|
+
*
|
|
57
|
+
* 이 둘은 「messages 안에 무엇이 살아 있나」 의 그림자다. messages 를
|
|
58
|
+
* 갈래마다 따로 두면서 이것만 세션에 하나로 두면 반드시 어긋나고, 어긋나면
|
|
59
|
+
* 「앞에서 읽은 그대로입니다」 가 **그 글이 없는 갈래에서** 나온다 —
|
|
60
|
+
* 모델은 대화 어디에도 없는 글을 가리키는 쪽지를 받고, 다시 읽어도 파일이
|
|
61
|
+
* 안 바뀌었으니 같은 쪽지를 받는다. 세 번째에 헛돈다고 턴이 죽는다.
|
|
62
|
+
* (agent/filemem.js 머리말의 「통째로 다시 싣는 것보다 훨씬 나쁘다」)
|
|
63
|
+
*/
|
|
52
64
|
this.갈래들 = [{
|
|
53
65
|
이름: '본줄기', messages: session.messages, usage: session.usage, todos: ctx?.todos ?? null,
|
|
54
66
|
할일: session.할일, 이번요청: session.이번요청, store: 첫store,
|
|
67
|
+
파일기억: session.파일기억, filesRead: session.filesRead,
|
|
55
68
|
}];
|
|
56
69
|
this.자리 = 0;
|
|
57
70
|
this.센것 = 1;
|
|
@@ -72,6 +85,9 @@ export class Threads {
|
|
|
72
85
|
// 적어 버린다 — 이어받을 때 하지도 않은 일이 본줄기에 남는다.
|
|
73
86
|
g.할일 = this.session.할일;
|
|
74
87
|
g.이번요청 = this.session.이번요청;
|
|
88
|
+
// 읽은 파일 기억도 같이 — 까닭은 생성자에 적어 뒀다.
|
|
89
|
+
g.파일기억 = this.session.파일기억;
|
|
90
|
+
g.filesRead = this.session.filesRead;
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
/** 갈래 하나를 화면으로 꺼낸다. */
|
|
@@ -83,6 +99,15 @@ export class Threads {
|
|
|
83
99
|
if (this.ctx) this.ctx.todos = g.todos;
|
|
84
100
|
this.session.할일 = g.할일 ?? [];
|
|
85
101
|
this.session.이번요청 = g.이번요청 ?? '';
|
|
102
|
+
/*
|
|
103
|
+
* 여기서 session.파일기억 을 통째로 갈아 끼운다.
|
|
104
|
+
*
|
|
105
|
+
* 이미 잊기() 를 부르는 자리들(compact.js · loop.js · session.js)은 전부
|
|
106
|
+
* session.파일기억 을 거치므로 손댈 것이 없다 — 그때그때 지금 갈래의 것을
|
|
107
|
+
* 잊는다. 문이 하나로 남는다.
|
|
108
|
+
*/
|
|
109
|
+
if (g.파일기억) this.session.파일기억 = g.파일기억;
|
|
110
|
+
if (g.filesRead) this.session.filesRead = g.filesRead;
|
|
86
111
|
// 적는 자리도 지금 갈래의 파일로 옮긴다 (agent/store.js 의 살림따라가기).
|
|
87
112
|
g.store?.살림따라가기?.(this.session);
|
|
88
113
|
// 상태줄이 지금 어느 갈래인지 보여 줄 수 있게 남긴다.
|
|
@@ -110,6 +135,14 @@ export class Threads {
|
|
|
110
135
|
할일: [],
|
|
111
136
|
이번요청: '',
|
|
112
137
|
store,
|
|
138
|
+
/*
|
|
139
|
+
* 갈라낸 것이면 지금 기억을 **베껴** 물려준다. 같이 쓰면 한쪽에서 파일을
|
|
140
|
+
* 다시 읽은 것이 다른 쪽 답을 바꾼다 — 갈라낸 쪽에서 v3 을 읽으면
|
|
141
|
+
* 본줄기가 「안 바뀌었다」 고 답하기 시작한다. 본줄기에 실린 것은 v1 인데도.
|
|
142
|
+
* 빈 갈래면 빈 기억이다. 대화가 비었으니 기억할 것도 없다.
|
|
143
|
+
*/
|
|
144
|
+
파일기억: 물려줄것 ? (this.session.파일기억?.베끼기?.() ?? new 파일기억()) : new 파일기억(),
|
|
145
|
+
filesRead: 물려줄것 ? new Map(this.session.filesRead ?? []) : new Map(),
|
|
113
146
|
};
|
|
114
147
|
this.갈래들.push(g);
|
|
115
148
|
// 갈라낸 것은 지금까지 오간 말을 새 파일에도 적어 둔다. 안 적으면 그 갈래를
|
package/src/cmdnames.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// 슬래시 명령 이름표. 이름과 인자 유무만 — 사람이 읽는 글은 i18n 쪽에 있다.
|
|
2
|
+
//
|
|
3
|
+
// ── 왜 파일이 따로인가 ──────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// 이 표를 보고 싶은 자리가 commands.js 바깥에도 있다. `deel run /이름` 이
|
|
6
|
+
// 그렇다 — 비대화 모드는 「이 이름이 대화 화면 전용인가」 를 알아야 "모르는
|
|
7
|
+
// 명령" 대신 "그건 대화 화면에서만 됩니다" 라고 말해 줄 수 있다.
|
|
8
|
+
//
|
|
9
|
+
// 그렇다고 oneshot.js 가 commands.js 를 부를 수는 없다. 그쪽은 대화 화면
|
|
10
|
+
// 한 벌을 통째로 끌고 온다 — 미리보기 서버 · 클립보드 · 화면 그리기까지.
|
|
11
|
+
// 배치로 도는 길에 그것이 딸려 들어오면 켜는 데만 시간이 늘고, 언젠가 그
|
|
12
|
+
// 안의 무엇이 사람을 기다리기 시작하면 배치가 그 자리에서 선다. 이 파일이
|
|
13
|
+
// 들여오는 것이 하나도 없는 것은 그래서다 — 표는 그냥 표다.
|
|
14
|
+
//
|
|
15
|
+
// desc·arg 를 게터로 만드는 일(COMMANDS)은 commands.js 에 그대로 뒀다.
|
|
16
|
+
// 그쪽은 i18n 을 부르므로 여기 두면 이 파일이 더는 순수한 표가 아니다.
|
|
17
|
+
|
|
18
|
+
export const 명령들 = {
|
|
19
|
+
help: { arg: false },
|
|
20
|
+
clear: { arg: false },
|
|
21
|
+
context: { arg: false },
|
|
22
|
+
ctx: { arg: true },
|
|
23
|
+
out: { arg: true },
|
|
24
|
+
grade: { arg: true },
|
|
25
|
+
compact: { arg: false },
|
|
26
|
+
model: { arg: true },
|
|
27
|
+
think: { arg: true },
|
|
28
|
+
mode: { arg: true },
|
|
29
|
+
work: { arg: true },
|
|
30
|
+
auto: { arg: false },
|
|
31
|
+
code: { arg: false },
|
|
32
|
+
plan: { arg: false },
|
|
33
|
+
architect: { arg: false },
|
|
34
|
+
debug: { arg: false },
|
|
35
|
+
ask: { arg: false },
|
|
36
|
+
orchestrator: { arg: false },
|
|
37
|
+
undo: { arg: true },
|
|
38
|
+
diff: { arg: true },
|
|
39
|
+
preview: { arg: true },
|
|
40
|
+
tools: { arg: false },
|
|
41
|
+
skills: { arg: true },
|
|
42
|
+
plugin: { arg: true },
|
|
43
|
+
cost: { arg: false },
|
|
44
|
+
status: { arg: false },
|
|
45
|
+
scan: { arg: true },
|
|
46
|
+
thread: { arg: true },
|
|
47
|
+
learned: { arg: true },
|
|
48
|
+
pin: { arg: true },
|
|
49
|
+
evidence: { arg: true },
|
|
50
|
+
commit: { arg: true },
|
|
51
|
+
review: { arg: false },
|
|
52
|
+
paste: { arg: false },
|
|
53
|
+
sessions: { arg: false },
|
|
54
|
+
recall: { arg: true },
|
|
55
|
+
mcp: { arg: false },
|
|
56
|
+
memory: { arg: true },
|
|
57
|
+
level: { arg: true },
|
|
58
|
+
bell: { arg: true },
|
|
59
|
+
keys: { arg: false },
|
|
60
|
+
motion: { arg: true },
|
|
61
|
+
consult: { arg: true },
|
|
62
|
+
lang: { arg: true },
|
|
63
|
+
lsp: { arg: true },
|
|
64
|
+
export: { arg: true },
|
|
65
|
+
init: { arg: false },
|
|
66
|
+
exit: { arg: false },
|
|
67
|
+
quit: { arg: false },
|
|
68
|
+
};
|
package/src/commands.js
CHANGED
|
@@ -13,7 +13,9 @@ import { 지금모드, 바깥인가, 나갈수있나 } from './safety/runmode.js
|
|
|
13
13
|
import { 주소가리기 } from './safety/secrets.js';
|
|
14
14
|
import { pick, confirm } from './ui/prompt.js';
|
|
15
15
|
import { load, save, 저장시도, resolveKey, upsert, 열쇠보관, configPath } from './config.js';
|
|
16
|
-
import { 지금상태 as
|
|
16
|
+
import { 지금상태 as 지금열쇠상태, 받기설정, 잊기 as 받은열쇠잊기 } from './safety/authcmd.js';
|
|
17
|
+
// 열쇠받기 명령을 정책이 못박아 뒀을 수 있다 — 받기설정 이 그 값을 같이 본다.
|
|
18
|
+
import { 정책읽기 } from './safety/policy.js';
|
|
17
19
|
import { 제공자고르기 } from './providers/index.js';
|
|
18
20
|
import { 종, 알릴만한초 } from './ui/notify.js';
|
|
19
21
|
/*
|
|
@@ -69,81 +71,13 @@ import { readTextFull } from './tools/fsutil.js';
|
|
|
69
71
|
import { 띄우기, 브라우저로 } from './preview/serve.js';
|
|
70
72
|
import { 클립보드그림, 그림앉히기 } from './tools/clipboard.js';
|
|
71
73
|
import { 크기말 } from './backend/vision.js';
|
|
74
|
+
import { 명령들 } from './cmdnames.js';
|
|
72
75
|
const MODES = {
|
|
73
76
|
auto: '자율 — 전부 알아서. 되돌리기가 안전망',
|
|
74
77
|
confirm: '확인 — 되돌릴 수 없는 것만 물어봄',
|
|
75
78
|
strict: '엄격 — 파일 변경·명령 전부 물어봄',
|
|
76
79
|
};
|
|
77
80
|
|
|
78
|
-
/*
|
|
79
|
-
* 슬래시 명령 목록.
|
|
80
|
-
*
|
|
81
|
-
* ── 왜 설명이 여기 없나 ─────────────────────────────────────────────────
|
|
82
|
-
*
|
|
83
|
-
* 이름과 인자 유무만 여기 두고, **사람이 읽는 글은 i18n/ko.js 에 있다.**
|
|
84
|
-
* 명령 이름은 원래부터 영어였는데(help·model·undo…) 그 옆의 설명이 전부
|
|
85
|
-
* 한글이라, 저장소를 처음 연 영어권 사람은 `/help` 를 쳐 봐도 무엇을 하는
|
|
86
|
-
* 명령인지 알 수가 없었다. 그래서 설명만 갈아 끼울 수 있게 뺐다.
|
|
87
|
-
*
|
|
88
|
-
* desc·arg 를 **게터로** 둔다. 켤 때 한 번 읽어 굳히면 /lang 으로 바꿔도
|
|
89
|
-
* 목록이 안 따라오고, 그러면 바뀐 것처럼 보이다가 안 바뀐 자리가 남는다.
|
|
90
|
-
* 볼 때마다 지금 언어로 읽는 편이 어긋날 자리가 없다.
|
|
91
|
-
*
|
|
92
|
-
* 없는 열쇠는 열쇠 이름이 그대로 나온다 — 빈칸이 아니다. 명령을 새로 넣고
|
|
93
|
-
* ko.js 에 적는 것을 빠뜨리면 `cmd.foo.desc` 가 화면에 찍혀서 바로 눈에 띈다.
|
|
94
|
-
* (검사도 그걸 잡는다.)
|
|
95
|
-
*/
|
|
96
|
-
const 명령들 = {
|
|
97
|
-
help: { arg: false },
|
|
98
|
-
clear: { arg: false },
|
|
99
|
-
context: { arg: false },
|
|
100
|
-
ctx: { arg: true },
|
|
101
|
-
out: { arg: true },
|
|
102
|
-
grade: { arg: true },
|
|
103
|
-
compact: { arg: false },
|
|
104
|
-
model: { arg: true },
|
|
105
|
-
think: { arg: true },
|
|
106
|
-
mode: { arg: true },
|
|
107
|
-
work: { arg: true },
|
|
108
|
-
auto: { arg: false },
|
|
109
|
-
code: { arg: false },
|
|
110
|
-
plan: { arg: false },
|
|
111
|
-
architect: { arg: false },
|
|
112
|
-
debug: { arg: false },
|
|
113
|
-
ask: { arg: false },
|
|
114
|
-
orchestrator: { arg: false },
|
|
115
|
-
undo: { arg: true },
|
|
116
|
-
diff: { arg: true },
|
|
117
|
-
preview: { arg: true },
|
|
118
|
-
tools: { arg: false },
|
|
119
|
-
skills: { arg: true },
|
|
120
|
-
plugin: { arg: true },
|
|
121
|
-
cost: { arg: false },
|
|
122
|
-
status: { arg: false },
|
|
123
|
-
scan: { arg: true },
|
|
124
|
-
thread: { arg: true },
|
|
125
|
-
learned: { arg: true },
|
|
126
|
-
pin: { arg: true },
|
|
127
|
-
evidence: { arg: true },
|
|
128
|
-
commit: { arg: true },
|
|
129
|
-
review: { arg: false },
|
|
130
|
-
paste: { arg: false },
|
|
131
|
-
sessions: { arg: false },
|
|
132
|
-
recall: { arg: true },
|
|
133
|
-
mcp: { arg: false },
|
|
134
|
-
memory: { arg: true },
|
|
135
|
-
level: { arg: true },
|
|
136
|
-
bell: { arg: true },
|
|
137
|
-
keys: { arg: false },
|
|
138
|
-
motion: { arg: true },
|
|
139
|
-
consult: { arg: true },
|
|
140
|
-
lang: { arg: true },
|
|
141
|
-
lsp: { arg: true },
|
|
142
|
-
export: { arg: true },
|
|
143
|
-
init: { arg: false },
|
|
144
|
-
exit: { arg: false },
|
|
145
|
-
quit: { arg: false },
|
|
146
|
-
};
|
|
147
81
|
|
|
148
82
|
/**
|
|
149
83
|
* 화면에 낼 명령표. desc·arg 는 볼 때마다 지금 언어로 읽는다.
|
|
@@ -499,7 +433,7 @@ export async function handle(line, session, ctx) {
|
|
|
499
433
|
}
|
|
500
434
|
|
|
501
435
|
const { 있는것, 없는것 } = 둘러보기();
|
|
502
|
-
const 뿌리것 = ctx?.scope?.root ? 프로젝트갈래(ctx.scope.root) : null;
|
|
436
|
+
const 뿌리것 = ctx?.scope?.root ? await 프로젝트갈래(ctx.scope.root) : null;
|
|
503
437
|
const 떠있는것 = 지금것들();
|
|
504
438
|
say('');
|
|
505
439
|
if (있는것.length) {
|
|
@@ -2684,6 +2618,9 @@ function 모델급(session, arg = '') {
|
|
|
2684
2618
|
|
|
2685
2619
|
if (값 === 'auto' || 값 === '자동') {
|
|
2686
2620
|
session.급정한것 = null;
|
|
2621
|
+
// 급은 턴마다 한 번만 맨다(session.js 의 급 머리말). 사람이 여기서 정한
|
|
2622
|
+
// 것은 다음 턴이 아니라 **지금** 먹어야 하니 빗장을 풀어 준다.
|
|
2623
|
+
session.급다시재기?.();
|
|
2687
2624
|
const g = session.급();
|
|
2688
2625
|
say('');
|
|
2689
2626
|
say(` ${c.cyan('◈')} 모델 급을 다시 ${c.bold('스스로 잡게')} 했습니다 — 지금은 ${c.white(g.급)}`);
|
|
@@ -2693,6 +2630,7 @@ function 모델급(session, arg = '') {
|
|
|
2693
2630
|
|
|
2694
2631
|
if (값 && 별명[값]) {
|
|
2695
2632
|
session.급정한것 = 별명[값];
|
|
2633
|
+
session.급다시재기?.();
|
|
2696
2634
|
const v = session.급값();
|
|
2697
2635
|
say('');
|
|
2698
2636
|
say(` ${c.cyan('◈')} 모델 급을 ${c.bold(별명[값])} 으로 정했습니다.`);
|
|
@@ -2853,7 +2791,47 @@ function 연결적용(session, p, ctx = null) {
|
|
|
2853
2791
|
kind: p.kind, base: p.baseUrl, auth: p.auth, key: resolveKey(p), model: p.model,
|
|
2854
2792
|
ctx: p.ctx, maxTokens: p.maxTokens ?? null,
|
|
2855
2793
|
streaming: p.streaming, tools: p.tools, json: p.json, think: p.think,
|
|
2794
|
+
/*
|
|
2795
|
+
* ── 이 두 줄이 없어서 회사 토큰이 남의 창구로 나갔다 ────────────────
|
|
2796
|
+
*
|
|
2797
|
+
* conn 을 짓는 자리는 넷이다 — repl.js · oneshot.js · acp/serve.js, 그리고
|
|
2798
|
+
* 여기. 앞의 셋은 이 둘을 넣는데 여기만 빠져 있었다. 그래서 `/model` 로
|
|
2799
|
+
* 갈아타면 **옛 프로필의 것이 그대로 남았다.**
|
|
2800
|
+
*
|
|
2801
|
+
* 회사 프로필(열쇠받기로 SSO 토큰을 받아 온다) → /model 남의창구
|
|
2802
|
+
* · 살아 있는 회사 토큰이 `Authorization` 에 실려 남의 host 로 간다
|
|
2803
|
+
* · 새 프로필의 제 열쇠(conn.key)는 **쓰이지도 않는다** —
|
|
2804
|
+
* adapter.js 의 머리말짓기 는 열쇠받기가 있으면 그쪽을 쓴다
|
|
2805
|
+
* · 남의 창구는 그 토큰을 모르니 401, 그러면 로그인 명령을 다시 돌린다
|
|
2806
|
+
*
|
|
2807
|
+
* 반대쪽도 같다. 맨 프로필에서 회사 프로필로 옮기면 열쇠받기가 null 로
|
|
2808
|
+
* 남아 401 뒤에 열쇠를 다시 받는 길이 잠긴다(adapter.js 의 열쇠다시받을까).
|
|
2809
|
+
*
|
|
2810
|
+
* vision 도 같은 값이다 — 안 옮기면 그림을 못 보는 모델에 그림을 보낸다.
|
|
2811
|
+
*/
|
|
2812
|
+
열쇠받기: 받기설정(p, { 정책값: 정책읽기().값 }),
|
|
2813
|
+
vision: p.vision ?? false,
|
|
2856
2814
|
});
|
|
2815
|
+
/*
|
|
2816
|
+
* 들고 있던 토큰도 버린다.
|
|
2817
|
+
*
|
|
2818
|
+
* 열쇠받기 설정을 새것으로 갈아도, authcmd.js 가 **모듈 전역 하나**에
|
|
2819
|
+
* 받은 토큰을 들고 있다(받은것). 어느 프로필 것인지는 안 적혀 있다. 그래서
|
|
2820
|
+
* 안 버리면 새 창구의 첫 요청에 옛 회사 토큰이 그대로 실려 나간다 — 위에서
|
|
2821
|
+
* 고친 것을 캐시가 도로 되돌리는 셈이다.
|
|
2822
|
+
*
|
|
2823
|
+
* 이 함수의 머리말이 「검사와 `/model` 갈아타기가 부른다」 라고 적혀 있었는데
|
|
2824
|
+
* 부르는 곳이 검사밖에 없었다. 이제 적힌 대로가 된다.
|
|
2825
|
+
*/
|
|
2826
|
+
받은열쇠잊기();
|
|
2827
|
+
/*
|
|
2828
|
+
* 급도 다시 맨다.
|
|
2829
|
+
*
|
|
2830
|
+
* 급은 conn 을 보고 매긴다(agent/grade.js 의 매김). 모델을 갈아탔는데 앞
|
|
2831
|
+
* 모델로 매긴 급을 턴 끝까지 들고 있으면, 화면에 적힌 급과 프롬프트에 실린
|
|
2832
|
+
* 급이 서로 다른 모델 이야기가 된다.
|
|
2833
|
+
*/
|
|
2834
|
+
session.급다시재기?.();
|
|
2857
2835
|
// 자물쇠도 같이 옮긴다. 이걸 빼먹으면 옛 주소가 열린 채로 남고 새 주소는 막혀
|
|
2858
2836
|
// 다음 한마디에서 바로 "허용되지 않은 주소" 가 난다.
|
|
2859
2837
|
allowEndpoint(p.baseUrl);
|
package/src/lsp/servers.js
CHANGED
|
@@ -193,13 +193,17 @@ export function 둘러보기(env = process.env) {
|
|
|
193
193
|
*
|
|
194
194
|
* 뿌리마다 한 번만 센다. 폴더 훑기는 값이 있고, 세션 중에 언어가 바뀌지 않는다.
|
|
195
195
|
*/
|
|
196
|
-
export function 프로젝트갈래(뿌리, env = process.env) {
|
|
196
|
+
export async function 프로젝트갈래(뿌리, env = process.env) {
|
|
197
|
+
// 센 것이 아니라 **세는 약속**을 담아 둔다. 훑기가 비동기라, 다 세고 나서
|
|
198
|
+
// 담으면 겹쳐 부른 쪽이 둘 다 빈 칸을 보고 둘 다 훑는다 — 에디터에서 탭을
|
|
199
|
+
// 한꺼번에 열면 실제로 그렇게 된다.
|
|
197
200
|
if (_센것.has(뿌리)) return _센것.get(뿌리);
|
|
201
|
+
const 약속 = (async () => {
|
|
198
202
|
let 답 = null;
|
|
199
203
|
try {
|
|
200
204
|
const 셈 = new Map();
|
|
201
205
|
const 첫파일 = new Map();
|
|
202
|
-
for (const f of walk(뿌리, { limit: 4000 })) {
|
|
206
|
+
for (const f of await walk(뿌리, { limit: 4000 })) {
|
|
203
207
|
const g = 갈래(f.path);
|
|
204
208
|
if (!g) continue;
|
|
205
209
|
셈.set(g, (셈.get(g) ?? 0) + 1);
|
|
@@ -210,8 +214,10 @@ export function 프로젝트갈래(뿌리, env = process.env) {
|
|
|
210
214
|
if (고르기(g, env)) { 답 = { 갈래: g, 대표파일: 첫파일.get(g), 개수: 셈.get(g) }; break; }
|
|
211
215
|
}
|
|
212
216
|
} catch { 답 = null; }
|
|
213
|
-
_센것.set(뿌리, 답);
|
|
214
217
|
return 답;
|
|
218
|
+
})();
|
|
219
|
+
_센것.set(뿌리, 약속);
|
|
220
|
+
return 약속;
|
|
215
221
|
}
|
|
216
222
|
|
|
217
223
|
/** 시험이 쓴다 — 폴더를 새로 만들고 다시 셀 때. */
|
package/src/oneshot.js
CHANGED
|
@@ -24,7 +24,8 @@ import { 말 as 옮긴말 } from './i18n/index.js';
|
|
|
24
24
|
import { 알림채움 } from './backend/retry.js';
|
|
25
25
|
import { 전선붙이기, 세션이름짓기 } from './backend/wire.js';
|
|
26
26
|
import { newId } from './agent/store.js';
|
|
27
|
-
import { discover } from './skills/discover.js';
|
|
27
|
+
import { discover, loadCommand } from './skills/discover.js';
|
|
28
|
+
import { 명령들 } from './cmdnames.js';
|
|
28
29
|
import { allowEndpoint, setOffline } from './safety/network.js';
|
|
29
30
|
import { 지금모드, 바깥인가, 나갈수있나 } from './safety/runmode.js';
|
|
30
31
|
import { 주소가리기 } from './safety/secrets.js';
|
|
@@ -259,6 +260,65 @@ export async function runOnce(opts = {}) {
|
|
|
259
260
|
session.commands = found.commands;
|
|
260
261
|
session.plugins = found.plugins;
|
|
261
262
|
|
|
263
|
+
/*
|
|
264
|
+
* ── `deel run /이름` ──────────────────────────────────────────────────
|
|
265
|
+
*
|
|
266
|
+
* 사람이 대화 화면에서 쓰던 슬래시 명령을 배치에서도 그대로 부르게 한다.
|
|
267
|
+
* `.claude/commands/배포점검.md` 를 만들어 두고 아침마다 손으로 `/배포점검`
|
|
268
|
+
* 을 치고 있었으면, 그걸 야간 잡에 옮기는 데 드는 일이 이것 하나다.
|
|
269
|
+
*
|
|
270
|
+
* 여태는 `/배포점검` 이 **그냥 글자로** 모델에게 갔다. 슬래시가 붙은 낱말
|
|
271
|
+
* 하나를 받은 모델은 대개 "무슨 뜻인지 모르겠다" 고 답하고 0 으로 끝난다 —
|
|
272
|
+
* 아무 일도 안 했는데 배치는 초록불이다. 이 파일이 제일 피하려는 결말이다.
|
|
273
|
+
*
|
|
274
|
+
* 세 갈래로 끝난다.
|
|
275
|
+
* · 이 PC 에 파일로 있는 명령이면 → 본문을 펴서 그것을 시킨다
|
|
276
|
+
* · 대화 화면 전용 이름(/help·/undo…)이면 → 여기선 안 된다고 말하고 1
|
|
277
|
+
* · 아무것도 아니면 → 모르는 명령이라고 말하고 1
|
|
278
|
+
* 두 실패 모두 **종료코드가 0 이 아니다.** 스크립트가 보는 것은 그 숫자다.
|
|
279
|
+
*
|
|
280
|
+
* 첫 낱말에 슬래시·역슬래시가 또 있으면 명령이 아니라 경로다(`/mnt/d/일감`).
|
|
281
|
+
* 그건 건드리지 않고 시킨 말 그대로 둔다 — commands.js 의 경로처럼보이나 와
|
|
282
|
+
* 같은 기준이다.
|
|
283
|
+
*/
|
|
284
|
+
if (시킬말.startsWith('/')) {
|
|
285
|
+
const [부른이름 = '', ...나머지] = 시킬말.slice(1).trim().split(/\s+/);
|
|
286
|
+
const 경로인가 = 부른이름.includes('/') || 부른이름.includes('\\');
|
|
287
|
+
if (부른이름 && !경로인가) {
|
|
288
|
+
const 인자 = 나머지.join(' ');
|
|
289
|
+
const 낮춘 = 부른이름.toLowerCase();
|
|
290
|
+
/*
|
|
291
|
+
* 붙박이 이름을 **먼저** 본다 — 대화 화면과 차례가 같아야 한다.
|
|
292
|
+
*
|
|
293
|
+
* commands.js 는 switch 로 붙박이를 먼저 받고, 못 받은 것만 default 에서
|
|
294
|
+
* 찾아 쓴다. 그래서 대화 화면의 `/help` 는 언제나 도움말이다. 여기서
|
|
295
|
+
* 차례를 뒤집으면 이 PC 에 `ralph-loop:help` 같은 플러그인 명령이 깔린
|
|
296
|
+
* 순간 `deel run /help` 만 딴 것을 부른다 — 같은 글자가 창에 따라 다른
|
|
297
|
+
* 일을 하는 것이 제일 나쁘다. 깔린 것에 따라 뜻이 흔들리면 안 된다.
|
|
298
|
+
*/
|
|
299
|
+
const 붙박이 = !!명령들[낮춘];
|
|
300
|
+
// 찾는 차례는 대화 화면과 같다 — 적은 그대로 · 대소문자 무시 · 꼬리 이름.
|
|
301
|
+
const 찾은 = 붙박이 ? null : (found.commands.find((x) => x.name === 부른이름)
|
|
302
|
+
?? found.commands.find((x) => x.name.toLowerCase() === 낮춘)
|
|
303
|
+
?? found.commands.find((x) => x.name.split(':').pop() === 낮춘));
|
|
304
|
+
if (붙박이) {
|
|
305
|
+
return 못함('repl-only', `/${부른이름} 은 대화 화면에서만 도는 명령입니다.`
|
|
306
|
+
+ ' 여기서 되는 것은 파일로 적어 둔 슬래시 명령뿐입니다 (.claude/commands · .deel/commands).');
|
|
307
|
+
} else if (찾은) {
|
|
308
|
+
const { text, error } = loadCommand(찾은, 인자);
|
|
309
|
+
if (error) return 못함('command-read', `/${부른이름} 을 읽지 못했습니다 — ${error}`);
|
|
310
|
+
곁(` ${c.cyan('⌘')} ${찾은.name} ${c.gray(찾은.source)}`);
|
|
311
|
+
시킬말 = text;
|
|
312
|
+
} else {
|
|
313
|
+
const 비슷 = found.commands
|
|
314
|
+
.filter((x) => x.name.includes(낮춘) || 낮춘.includes(x.name.split(':').pop()))
|
|
315
|
+
.slice(0, 5).map((x) => '/' + x.name);
|
|
316
|
+
return 못함('no-command', `모르는 명령 /${부른이름}`
|
|
317
|
+
+ (비슷.length ? ` — 비슷한 것: ${비슷.join(' ')}` : ' — 이 폴더에서 찾은 슬래시 명령이 없습니다.'));
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
262
322
|
// ── 물어보는 자리를 전부 막는다 ────────────────────────────────────────
|
|
263
323
|
//
|
|
264
324
|
// 여기가 이 파일의 핵심이다. 물어보는 함수 하나라도 기다리게 두면 그것 하나로
|
package/src/pack/selfpack.js
CHANGED
|
@@ -199,7 +199,7 @@ export function reviewSheet(a, at, { lang = 언어() } = {}) {
|
|
|
199
199
|
L.push(' 아예 띄우지 않습니다.');
|
|
200
200
|
L.push('');
|
|
201
201
|
L.push(' --offline 으로 켜면 [B]·[C]·[D] 가 모두 막히고, 이 컴퓨터 안으로만 다닙니다.');
|
|
202
|
-
L.push(' (검증: npm test 안의 network / web / mcp
|
|
202
|
+
L.push(' (검증: npm test 안의 network / web / mcp 검사가 이를 확인합니다 — 항목 수는 그 출력에 있습니다)');
|
|
203
203
|
L.push('');
|
|
204
204
|
|
|
205
205
|
L.push('4. 스킬·플러그인');
|
|
@@ -209,7 +209,20 @@ export function reviewSheet(a, at, { lang = 언어() } = {}) {
|
|
|
209
209
|
L.push(' (검증: npm test 안의 no-bundle 검사가 이를 확인합니다)');
|
|
210
210
|
L.push('');
|
|
211
211
|
|
|
212
|
-
L.push(
|
|
212
|
+
L.push('5. 검사가 무엇을 지키는가');
|
|
213
|
+
L.push(줄());
|
|
214
|
+
L.push(' 검사가 초록이라는 것은 「아무도 안 깨졌다」 는 뜻이지 「깨면 잡힌다」 는 뜻이');
|
|
215
|
+
L.push(' 아닙니다. 이 둘은 겉으로 구별이 안 되는데 값어치는 정반대입니다.');
|
|
216
|
+
L.push(' 그래서 지켜야 할 줄을 일부러 어긋내 놓고, 짝지은 검사가 빨개지는지 되잽니다.');
|
|
217
|
+
L.push(' 무엇을 어긋내는지와 「그러면 무슨 일이 나는지」 는 test/mutants.json 에 적어');
|
|
218
|
+
L.push(' 두었습니다 — 살림 폴더 통째로 지우기 · 되돌리기 스냅샷 · ACP 문 파리티 ·');
|
|
219
|
+
L.push(' 도움말의 종료코드 표가 그 안에 있습니다. 하나라도 안 잡히면 실패입니다.');
|
|
220
|
+
L.push(' (검증: npm run mutate — 잡은 수와 샌 수가 그 출력에 있습니다)');
|
|
221
|
+
L.push(' 검사와 이 목록은 이 묶음에 안 들어 있습니다 — 묶음에는 도는 코드만 담습니다.');
|
|
222
|
+
L.push(' 저장소에서 보실 수 있습니다: https://github.com/jysvai/deel-local-cli');
|
|
223
|
+
L.push('');
|
|
224
|
+
|
|
225
|
+
L.push(`6. 담긴 파일 ${a.files.length}개 — SHA-256`);
|
|
213
226
|
L.push(줄());
|
|
214
227
|
const w = Math.max(...a.files.map((f) => f.path.length));
|
|
215
228
|
for (const f of a.files) {
|
package/src/pack/sheet.en.js
CHANGED
|
@@ -106,7 +106,7 @@ export function reviewSheetEn(a, at, probes) {
|
|
|
106
106
|
L.push(' filtering its requests, --offline does not start the server at all.');
|
|
107
107
|
L.push('');
|
|
108
108
|
L.push(' Starting with --offline blocks [B], [C] and [D]; traffic stays on this machine.');
|
|
109
|
-
L.push(' (Verified by the network / web / mcp checks in npm test
|
|
109
|
+
L.push(' (Verified by the network / web / mcp checks in npm test; the count is in that output.)');
|
|
110
110
|
L.push('');
|
|
111
111
|
|
|
112
112
|
L.push('4. Skills and plugins');
|
|
@@ -116,7 +116,21 @@ export function reviewSheetEn(a, at, probes) {
|
|
|
116
116
|
L.push(' (Verified by the no-bundle check in npm test.)');
|
|
117
117
|
L.push('');
|
|
118
118
|
|
|
119
|
-
L.push(
|
|
119
|
+
L.push('5. What the tests actually guard');
|
|
120
|
+
L.push(rule());
|
|
121
|
+
L.push(' A green test run means "nothing broke", not "breaking it would be caught".');
|
|
122
|
+
L.push(' The two look identical and are worth the opposite.');
|
|
123
|
+
L.push(' So we deliberately break the lines that matter and check that the paired test');
|
|
124
|
+
L.push(' turns red. What gets broken, and what would go wrong if it stayed broken, is');
|
|
125
|
+
L.push(' written down in test/mutants.json - deleting the whole .deel folder, undo');
|
|
126
|
+
L.push(' snapshots, ACP door parity, and the exit-code table are among them.');
|
|
127
|
+
L.push(' A single survivor fails the run.');
|
|
128
|
+
L.push(' (Verified by npm run mutate; killed and survived counts are in that output.)');
|
|
129
|
+
L.push(' The tests and that list are not in this package - it ships only running code.');
|
|
130
|
+
L.push(' Read them in the repository: https://github.com/jysvai/deel-local-cli');
|
|
131
|
+
L.push('');
|
|
132
|
+
|
|
133
|
+
L.push(`6. ${a.files.length} files shipped - SHA-256`);
|
|
120
134
|
L.push(rule());
|
|
121
135
|
const w = Math.max(...a.files.map((f) => f.path.length));
|
|
122
136
|
for (const f of a.files) {
|
package/src/repl.js
CHANGED
|
@@ -411,7 +411,7 @@ export async function chatLoop(opts = {}) {
|
|
|
411
411
|
* 답이 바뀌지 않는다. 아무것도 안 깔아 준다 — 있으면 쓰고 없으면 없는 대로
|
|
412
412
|
* Grep·Outline 으로 간다 (lsp/servers.js 머리말).
|
|
413
413
|
*/
|
|
414
|
-
session.lsp = 언어서버있나(root);
|
|
414
|
+
session.lsp = await 언어서버있나(root);
|
|
415
415
|
// 지난 대화에서 정해 둔 것을 들고 시작한다.
|
|
416
416
|
// 기억은 Session 이 켤 때 직접 읽는다 (session.js 생성자) — deel run 도 같은 것을 들고 시작하게.
|
|
417
417
|
|
package/src/safety/guard.js
CHANGED
|
@@ -466,7 +466,25 @@ export function 경로낱말(cmd) {
|
|
|
466
466
|
t = t.replace(/^[<>]+/, '');
|
|
467
467
|
if (!t) continue;
|
|
468
468
|
const 풀린 = 자리표풀기(t);
|
|
469
|
-
|
|
469
|
+
/*
|
|
470
|
+
* ── 맨 낱말 `.deel` 도 자리로 친다 ──────────────────────────────────
|
|
471
|
+
*
|
|
472
|
+
* 이 필터는 `/` 나 `\` 가 있어야 「자리」 로 쳤다. 그래서 `rm -rf .deel`
|
|
473
|
+
* 의 `.deel` 은 **후보로도 안 잡혔다.** checkPaths 는 이 목록 안에서만
|
|
474
|
+
* 내부살림() 을 부르므로, 내부살림 을 아무리 고쳐도 이 철자는 안 걸린다.
|
|
475
|
+
*
|
|
476
|
+
* 그 결과가 이랬다 — 같은 파일을 지우는 두 명령이 서로 다른 답을 받았다.
|
|
477
|
+
*
|
|
478
|
+
* rm -rf .deel/history → 막힘
|
|
479
|
+
* rm -rf .deel → 통과
|
|
480
|
+
*
|
|
481
|
+
* 그래서 이 이름 하나만 예외로 둔다. **품고 있는 것이 아니라 그 이름
|
|
482
|
+
* 자체**일 때만이다 — `.deelignore` 는 다른 파일이고, `my.deel.txt` 도
|
|
483
|
+
* 남의 파일이다. 넓히다 반대로 베면 모델이 까닭 없이 무언가를 못 하게
|
|
484
|
+
* 되는데, 그것을 신고해 줄 사람이 없다.
|
|
485
|
+
*/
|
|
486
|
+
const 살림이름 = 풀린 === '.deel' || 풀린.toLowerCase() === '.deel';
|
|
487
|
+
const 경로같나 = 풀린.includes('/') || 풀린.includes('\\') || /^[a-zA-Z]:$/.test(풀린) || 살림이름;
|
|
470
488
|
if (!경로같나) continue;
|
|
471
489
|
/*
|
|
472
490
|
* 남은 자리표가 있으면(우리가 못 푼 것) **어디로 갈지 모른다.** 울타리
|
|
@@ -639,3 +657,58 @@ const MUTATING = /(^|[|;&]\s*)(sudo\s+)?(git\s+(commit|push|merge|rebase|reset)|
|
|
|
639
657
|
export function isMutating(cmd) {
|
|
640
658
|
return MUTATING.test(String(cmd));
|
|
641
659
|
}
|
|
660
|
+
|
|
661
|
+
/*
|
|
662
|
+
* 셸이 **파일에 쓰는 꼴**인가 — 스냅샷을 뜰지 정하는 데만 쓴다.
|
|
663
|
+
*
|
|
664
|
+
* ── 왜 MUTATING 을 넓히지 않고 새로 만드나 ──────────────────────────────
|
|
665
|
+
*
|
|
666
|
+
* MUTATING 은 세 곳에서 쓰인다.
|
|
667
|
+
*
|
|
668
|
+
* tools/index.js:307 스냅샷을 뜰지 ← 넓히고 싶은 곳
|
|
669
|
+
* agent/loop.js:1214 같은 명령을 두 번 안 돌린다
|
|
670
|
+
* agent/loop.js:1255 엄격 모드에서 물어본다
|
|
671
|
+
*
|
|
672
|
+
* 아래 것들을 MUTATING 에 넣으면 뒤의 두 곳이 같이 넓어진다. `echo x > log`
|
|
673
|
+
* 마다 승인 창이 뜨고, 두 번째 `echo` 가 「다시 실행하지 않습니다」 로 막힌다.
|
|
674
|
+
* 고치려던 것보다 나쁜 것이 생긴다. 그래서 **쓰임이 하나뿐인 술어**를 따로 둔다.
|
|
675
|
+
*
|
|
676
|
+
* ── 왜 넉넉해도 되나 ────────────────────────────────────────────────────
|
|
677
|
+
*
|
|
678
|
+
* 여기서 잘못 걸리면 안 떠도 될 파일을 하나 더 뜨는 것뿐이다. MUTATING 에서
|
|
679
|
+
* 잘못 걸리면 멀쩡한 명령이 막힌다. 값이 다른 두 실수라, 이쪽은 넉넉히 잡는다.
|
|
680
|
+
* `echo "a > b"` 처럼 글 안의 부등호까지 걸려도 손해가 스냅샷 한 장이다.
|
|
681
|
+
*
|
|
682
|
+
* ── 왜 이 목록이 필요했나 ───────────────────────────────────────────────
|
|
683
|
+
*
|
|
684
|
+
* 열한 가지 파일 고치기를 재 보니 MUTATING 에는 `rm` 하나만 걸렸다.
|
|
685
|
+
*
|
|
686
|
+
* O rm f.js
|
|
687
|
+
* X echo x > f.js X printf x >> f.js X sed -i s/a/b/ f.js
|
|
688
|
+
* X tee f.js X truncate -s 0 f.js X dd of=f.js
|
|
689
|
+
* X node -e "…" X python -c "…" X npx prettier --write src/
|
|
690
|
+
* X git checkout -- f.js
|
|
691
|
+
*
|
|
692
|
+
* 즉 모델이 셸에서 파일을 고치는 **흔한 방법 전부**가 스냅샷 없이 지나갔다.
|
|
693
|
+
* 그리고 스냅샷이 없으면 그 턴은 기록을 한 줄도 안 남겨서, /undo 가 그 턴의
|
|
694
|
+
* 존재를 모르고 **앞의 무관한 턴**을 되돌린 뒤 성공했다고 적었다.
|
|
695
|
+
* (검사: test/undo.test.js 의 「셸쓰기-다른턴을-되돌리던-것」)
|
|
696
|
+
*/
|
|
697
|
+
const 셸쓰기무늬 = [
|
|
698
|
+
/>{1,2}\s*(?![&>])\S/, // echo x > f · printf x >> f (2>&1 은 & 라 안 걸린다)
|
|
699
|
+
/\bsed\s+(-[a-z]*i\b|--in-place)/i, // sed -i · sed --in-place
|
|
700
|
+
/\bperl\s+(-[a-z]*i\b|--in-place)/i,
|
|
701
|
+
/\btee\b/i,
|
|
702
|
+
/\b(set-content|add-content|out-file)\b/i, // 파워셸
|
|
703
|
+
/\btruncate\b/i,
|
|
704
|
+
/\bdd\b[^|;&]*\bof=/i,
|
|
705
|
+
/--write\b/i, // prettier --write 류
|
|
706
|
+
/\bpatch\b/i,
|
|
707
|
+
/\bgit\s+(checkout\s+--|restore)\b/i, // 작업 트리를 되돌리는 것도 파일을 바꾼다
|
|
708
|
+
];
|
|
709
|
+
|
|
710
|
+
/** 이 명령이 파일 내용을 바꿀 낌새인가. **스냅샷 판단에만** 쓴다. */
|
|
711
|
+
export function 셸이파일에쓰나(cmd) {
|
|
712
|
+
const s = String(cmd ?? '');
|
|
713
|
+
return 셸쓰기무늬.some((무늬) => 무늬.test(s));
|
|
714
|
+
}
|
package/src/safety/undo.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 되돌리기. 승인 프롬프트를 안 쓰는 대신 이게 안전망이다.
|
|
2
2
|
// 파일을 고치기 전에 항상 이전 내용을 떠 놓고, /undo 로 턴 단위로 되돌린다.
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, appendFileSync, statSync } from 'node:fs';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, appendFileSync, statSync, chmodSync } from 'node:fs';
|
|
5
5
|
import { looksBinary } from '../tools/encoding.js';
|
|
6
6
|
import { 말 } from '../i18n/index.js';
|
|
7
7
|
|
|
@@ -12,9 +12,40 @@ const KEEP_TURNS = 50;
|
|
|
12
12
|
export class History {
|
|
13
13
|
constructor(root) {
|
|
14
14
|
this.dir = join(root, '.deel', 'history');
|
|
15
|
-
|
|
15
|
+
// 폴더부터 본인 것으로. 파일만 잠그고 폴더가 0755 면 이름은 다 보인다.
|
|
16
|
+
mkdirSync(this.dir, { recursive: true, mode: 0o700 });
|
|
16
17
|
this.file = join(this.dir, 'edits.jsonl');
|
|
17
18
|
this.turn = 0;
|
|
19
|
+
/** 잠근 결과. 밖에서 볼 수 있어야 「잠갔다」 는 말이 뜻을 갖는다. */
|
|
20
|
+
this.잠금 = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/*
|
|
24
|
+
* ── 이 파일은 남의 파일 **원문**을 담는다 ───────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* 되돌리려면 고치기 전 내용을 그대로 들고 있어야 한다. 그래서 여기에는
|
|
27
|
+
* `.env` · `id_rsa` · `.npmrc` · kubeconfig 가 **평문으로** 들어온다.
|
|
28
|
+
* 가리지도 않는다 — 가리면 되돌릴 때 사람의 진짜 열쇠가 표로 덮여 없어진다.
|
|
29
|
+
* 즉 이 자리에서 지키는 방법은 파일 권한 하나뿐인데, 그것이 없었다.
|
|
30
|
+
*
|
|
31
|
+
* umask 022 인 리눅스·맥에서 이 파일은 0644 로 만들어진다. 같은 PC 의 다른
|
|
32
|
+
* 계정, 데몬, 공유 홈(NFS)의 아무나가 deel 이 손댄 모든 파일의 원문을 읽는다
|
|
33
|
+
* — 주인이 일부러 0600 으로 잠가 둔 파일까지. `.deel` 아래 다른 기록
|
|
34
|
+
* (agent/store.js · safety/audit.js)은 전부 잠그는데 여기만 안 잠갔다.
|
|
35
|
+
*
|
|
36
|
+
* 1.14.0 에서 셸 쓰기(`sed -i` · `echo >` · `tee`)까지 스냅샷을 뜨게 하면서
|
|
37
|
+
* 들어오는 파일이 더 늘었다. 고친 것이 이 구멍을 넓힌 셈이라 같이 막는다.
|
|
38
|
+
*
|
|
39
|
+
* 윈도우(NTFS)에서는 chmod 가 아무 일도 안 한다. 거기서는 못 잠근 것이지
|
|
40
|
+
* 잠근 척하지 않는다 — 그래서 결과를 `잠금` 에 있는 그대로 남긴다.
|
|
41
|
+
* (safety/audit.js · agent/store.js 와 같은 모양이다)
|
|
42
|
+
*/
|
|
43
|
+
#잠갔나 = false;
|
|
44
|
+
#잠그기() {
|
|
45
|
+
if (this.#잠갔나) return;
|
|
46
|
+
this.#잠갔나 = true;
|
|
47
|
+
try { chmodSync(this.file, 0o600); this.잠금 = { 모드: 0o600 }; }
|
|
48
|
+
catch (err) { this.잠금 = { 못함: err?.code ?? String(err) }; }
|
|
18
49
|
}
|
|
19
50
|
|
|
20
51
|
/**
|
|
@@ -73,6 +104,7 @@ export class History {
|
|
|
73
104
|
if (뜬것.enc) rec.enc = 뜬것.enc;
|
|
74
105
|
if (뜬것.skipped) rec.skipped = 뜬것.skipped;
|
|
75
106
|
appendFileSync(this.file, JSON.stringify(rec) + '\n', 'utf8');
|
|
107
|
+
this.#잠그기();
|
|
76
108
|
this.#이번턴.set(absPath, rec);
|
|
77
109
|
this.#maybePrune();
|
|
78
110
|
return rec;
|
|
@@ -109,6 +141,8 @@ export class History {
|
|
|
109
141
|
if (!버린수) return 0;
|
|
110
142
|
try {
|
|
111
143
|
writeFileSync(this.file, 남길것.map((r) => JSON.stringify(r)).join('\n') + (남길것.length ? '\n' : ''), 'utf8');
|
|
144
|
+
// 통째로 다시 쓰면 새 파일일 수 있다 — 빗장을 다시 건다 (agent/store.js 도 같다).
|
|
145
|
+
this.#잠갔나 = false; this.#잠그기();
|
|
112
146
|
} catch { return 0; }
|
|
113
147
|
return 버린수;
|
|
114
148
|
}
|
|
@@ -199,6 +233,7 @@ export class History {
|
|
|
199
233
|
const 못한것 = new Set(restored.filter((x) => x.ok === false).map((x) => x.path));
|
|
200
234
|
const keep = recs.filter((r) => !turns.includes(r.turn) || 못한것.has(r.path));
|
|
201
235
|
writeFileSync(this.file, keep.map((r) => JSON.stringify(r)).join('\n') + (keep.length ? '\n' : ''), 'utf8');
|
|
236
|
+
this.#잠갔나 = false; this.#잠그기();
|
|
202
237
|
return {
|
|
203
238
|
restored,
|
|
204
239
|
// 화면·감사기록이 쓰는 수. **진짜로 되돌아간 것만** 센다.
|