deel-local-cli 1.20.13 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.ko.md +97 -78
  2. package/README.md +120 -81
  3. package/bin/deel.js +398 -32
  4. package/package.json +2 -1
  5. package/src/acp/jsonrpc.js +87 -7
  6. package/src/acp/map.js +23 -3
  7. package/src/acp/serve.js +333 -25
  8. package/src/agent/agents.js +110 -12
  9. package/src/agent/askcheck.js +50 -6
  10. package/src/agent/asks.js +59 -5
  11. package/src/agent/budget.js +9 -2
  12. package/src/agent/card.js +8 -2
  13. package/src/agent/commit.js +168 -16
  14. package/src/agent/compact.js +93 -21
  15. package/src/agent/{/354/213/240/353/242/260/353/217/204.js → confidence.js} +1 -1
  16. package/src/agent/effort.js +85 -15
  17. package/src/agent/evidence.js +63 -7
  18. package/src/agent/evolve.js +132 -24
  19. package/src/agent/grade.js +12 -3
  20. package/src/agent/loop.js +408 -81
  21. package/src/agent/memory.js +176 -29
  22. package/src/agent/mention.js +22 -5
  23. package/src/agent/models.js +31 -7
  24. package/src/agent/modes.js +25 -10
  25. package/src/agent/outschema.js +381 -56
  26. package/src/agent/pins.js +33 -2
  27. package/src/agent/project.js +73 -8
  28. package/src/agent/recall.js +40 -9
  29. package/src/agent/{/354/204/261/355/225/234/352/270/260/353/241/235.js → recordshape.js} +19 -8
  30. package/src/agent/review.js +143 -13
  31. package/src/agent/route.js +253 -26
  32. package/src/agent/salvage.js +2 -1
  33. package/src/agent/session.js +457 -31
  34. package/src/agent/store.js +232 -22
  35. package/src/agent/threads.js +52 -3
  36. package/src/backend/adapter.js +508 -85
  37. package/src/backend/azure.js +12 -1
  38. package/src/backend/cachemark.js +31 -5
  39. package/src/backend/clientcert.js +23 -1
  40. package/src/backend/ctxsize.js +56 -9
  41. package/src/backend/detect.js +252 -19
  42. package/src/backend/http.js +383 -42
  43. package/src/backend/learn.js +101 -11
  44. package/src/backend/mcp.js +333 -24
  45. package/src/backend/price.js +61 -13
  46. package/src/backend/probe.js +188 -36
  47. package/src/backend/proxy.js +108 -14
  48. package/src/backend/quota.js +226 -30
  49. package/src/backend/retry.js +46 -3
  50. package/src/backend/scan.js +47 -10
  51. package/src/backend/scanui.js +57 -7
  52. package/src/backend/toolfit.js +204 -22
  53. package/src/backend/vision.js +14 -1
  54. package/src/backend/wire.js +58 -10
  55. package/src/cmdnames.js +16 -0
  56. package/src/commands/common.js +70 -0
  57. package/src/commands/extend.js +299 -0
  58. package/src/commands/model.js +851 -0
  59. package/src/commands/view.js +328 -0
  60. package/src/commands/work.js +823 -0
  61. package/src/commands.js +193 -2082
  62. package/src/completion.js +45 -3
  63. package/src/config.js +454 -41
  64. package/src/configexplain.js +127 -15
  65. package/src/doctor.js +164 -27
  66. package/src/i18n/en.js +22 -0
  67. package/src/i18n/index.js +31 -1
  68. package/src/i18n/ja.js +25 -1
  69. package/src/i18n/ko.js +25 -2
  70. package/src/i18n/zh.js +25 -1
  71. package/src/lsp/client.js +111 -9
  72. package/src/lsp/servers.js +86 -22
  73. package/src/oneshot.js +325 -65
  74. package/src/pack/sbom.js +28 -5
  75. package/src/pack/selfpack.js +54 -5
  76. package/src/pack/sheet.en.js +24 -3
  77. package/src/pack/tar.js +45 -8
  78. package/src/pack/zip.js +103 -8
  79. package/src/plugins/manage.js +165 -14
  80. package/src/preview/serve.js +239 -23
  81. package/src/providers/bedrock.js +30 -5
  82. package/src/providers/gemini.js +8 -0
  83. package/src/providers/index.js +17 -1
  84. package/src/repl.js +254 -61
  85. package/src/report.js +7 -2
  86. package/src/reset.js +151 -25
  87. package/src/safety/audit.js +108 -8
  88. package/src/safety/authcmd.js +75 -5
  89. package/src/safety/guard.js +531 -15
  90. package/src/safety/hooks.js +172 -20
  91. package/src/safety/keystore.js +178 -18
  92. package/src/safety/network.js +209 -6
  93. package/src/safety/policy.js +377 -36
  94. package/src/safety/runmode.js +9 -2
  95. package/src/safety/secrets.js +243 -17
  96. package/src/safety/shellenv.js +64 -3
  97. package/src/safety/trust.js +298 -32
  98. package/src/safety/undo.js +409 -17
  99. package/src/setup.js +175 -17
  100. package/src/skills/discover.js +122 -19
  101. package/src/stats.js +57 -16
  102. package/src/tools/{/355/231/225/354/235/270/353/262/225.js → checkmethods.js} +26 -1
  103. package/src/tools/clipboard.js +53 -20
  104. package/src/tools/convert.js +118 -37
  105. package/src/tools/desc.en.js +48 -5
  106. package/src/tools/doc2md.js +36 -4
  107. package/src/tools/docs.js +94 -18
  108. package/src/tools/edit-match.js +212 -13
  109. package/src/tools/encoding.js +525 -28
  110. package/src/tools/excel-com.js +63 -15
  111. package/src/tools/excel.js +12 -7
  112. package/src/tools/fastgrep.js +501 -38
  113. package/src/tools/fig.js +21 -4
  114. package/src/tools/fsutil.js +235 -21
  115. package/src/tools/hwpxwrite.js +80 -19
  116. package/src/tools/ignore.js +55 -7
  117. package/src/tools/index.js +1083 -148
  118. package/src/tools/jobs.js +328 -50
  119. package/src/tools/kiwi.js +50 -5
  120. package/src/tools/lsp.js +164 -32
  121. package/src/tools/outline.js +52 -12
  122. package/src/tools/pdf.js +367 -59
  123. package/src/tools/shell.js +12 -4
  124. package/src/tools/spawn.js +203 -18
  125. package/src/tools/todo.js +73 -4
  126. package/src/tools/verify.js +328 -45
  127. package/src/tools/webfetch.js +194 -25
  128. package/src/tools/xlsx.js +232 -31
  129. package/src/ui/ansi.js +209 -15
  130. package/src/ui/banner.js +2 -1
  131. package/src/ui/complete.js +46 -4
  132. package/src/ui/diff.js +4 -2
  133. package/src/ui/export.js +91 -17
  134. package/src/ui/inputbox.js +58 -18
  135. package/src/ui/intro.js +9 -2
  136. package/src/ui/level.js +32 -7
  137. package/src/ui/md.js +101 -15
  138. package/src/ui/motion.js +5 -2
  139. package/src/ui/notify.js +7 -1
  140. package/src/ui/office.js +37 -5
  141. package/src/ui/pastechip.js +3 -2
  142. package/src/ui/pick.js +85 -12
  143. package/src/ui/prompt.js +176 -13
  144. package/src/ui/screen.js +2 -1
  145. package/src/ui/spinner.js +6 -1
  146. package/src/ui/status.js +80 -12
  147. package/src/ui/wrap.js +23 -37
  148. /package/src/agent/{/353/213/250/352/263/204.js" → phase.js} +0 -0
  149. /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" → build-deep/SKILL.md} +0 -0
  150. /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" → debug-step-by-step/SKILL.md} +0 -0
  151. /package/src/skills/builtin/{/353/201/235/352/271/214/354/247/200-/355/225/230/352/270/260/SKILL.md" → finish-all/SKILL.md} +0 -0
  152. /package/src/skills/builtin/{/354/212/244/354/212/244/353/241/234-/352/262/200/355/206/240/SKILL.md" → self-review/SKILL.md} +0 -0
  153. /package/src/skills/builtin/{/354/275/224/353/223/234-/354/244/204/354/235/264/352/270/260/SKILL.md" → simplify-code/SKILL.md} +0 -0
  154. /package/src/skills/builtin/{/354/260/224/353/237/254/353/263/264/352/270/260/SKILL.md" → spike/SKILL.md} +0 -0
  155. /package/src/skills/builtin/{/352/262/200/354/202/254-/353/250/274/354/240/200/SKILL.md" → test-first/SKILL.md} +0 -0
@@ -35,6 +35,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs';
35
35
  import { homedir } from 'node:os';
36
36
  import { extname, join } from 'node:path';
37
37
  import { frontmatter } from '../skills/discover.js';
38
+ import { BOM떼기, 믿나, 고른경로 } from '../safety/trust.js';
38
39
 
39
40
  /** 한 폴더에서 읽을 수 있는 수. 스무 개를 넘기면 목록이 프롬프트를 먹는다. */
40
41
  export const 최대 = 24;
@@ -46,6 +47,20 @@ export const 자리들 = (밑) => [join(밑, '.deel', 'agents'), join(밑, '.cla
46
47
  /** 파일 이름에서 뽑은 이름. 확장자와 경로를 뗀 것. */
47
48
  const 이름뽑기 = (파일) => 파일.replace(/\.(json|md)$/i, '').trim();
48
49
 
50
+ /**
51
+ * 적힌 것 중 **처음으로 비지 않은** 것. 없으면 빈 글.
52
+ *
53
+ * `??` 는 빈 글에서 안 넘어간다. `name: ""` 한 줄이 파일 이름 대신 서던 자리가
54
+ * 그래서 생겼다 — 빈 이름은 안 적은 것과 같다(2.0.0 8회차).
55
+ */
56
+ const 첫글 = (...것들) => {
57
+ for (const x of 것들) {
58
+ const s = String(x ?? '').trim();
59
+ if (s) return s;
60
+ }
61
+ return '';
62
+ };
63
+
49
64
  /**
50
65
  * 정의 하나를 우리 모양으로 편다.
51
66
  *
@@ -53,11 +68,19 @@ const 이름뽑기 = (파일) => 파일.replace(/\.(json|md)$/i, '').trim();
53
68
  * 사람은 제가 만든 에이전트가 왜 안 보이는지 영영 모른다.
54
69
  */
55
70
  export function 한정의(이름, 것, 출처) {
71
+ const 쓸이름 = String(이름 ?? '').trim();
72
+ /*
73
+ * 이름이 없으면 안 받는다 (2.0.0 8회차 판정).
74
+ *
75
+ * 찾기() 는 빈 이름에 늘 null 을 준다 — 이름 빈 정의는 세워 봐야 영영 못 부른다.
76
+ * 그런데 상한(최대) 한 칸을 먹고, Task 스키마 목록에는 `(설명)` 만 덜렁 실렸다.
77
+ */
78
+ if (!쓸이름) return null;
56
79
  const 설명 = String(것?.설명 ?? 것?.description ?? '').trim();
57
80
  if (!설명) return null; // 설명이 없으면 부모 모델이 고를 수가 없다
58
81
  const 도구 = 것?.도구 ?? 것?.tools ?? null;
59
82
  return {
60
- 이름: String(이름).trim(),
83
+ 이름: 쓸이름,
61
84
  설명: 설명.length > 설명길이 ? `${설명.slice(0, 설명길이)}…` : 설명,
62
85
  모드: String(것?.모드 ?? 것?.mode ?? '').trim() || null,
63
86
  모델: String(것?.모델 ?? 것?.model ?? '').trim() || null,
@@ -84,8 +107,13 @@ function 파일하나(경로, 파일, 출처) {
84
107
  const 글 = readFileSync(경로, 'utf8');
85
108
  if (extname(파일).toLowerCase() === '.json') {
86
109
  let j;
87
- try { j = JSON.parse(글); } catch (e) { return { 왜: `${파일}: JSON 이 아닙니다 — ${e.message}` }; }
88
- const r = 한정의(j?.이름 ?? j?.name ?? 이름, j, 출처);
110
+ /*
111
+ * BOM 을 떼고 푼다. 파워셸 5.1 의 `Set-Content -Encoding UTF8` 이 파일 앞에 BOM 을
112
+ * 붙이고, 사내에서 정의를 스크립트로 까는 자리가 딱 그 명령이다. 안 떼면 그 한
113
+ * 글자에서 「JSON 이 아닙니다」 로 정의가 통째로 빠진다 (safety/trust.js 의 BOM떼기).
114
+ */
115
+ try { j = JSON.parse(BOM떼기(글)); } catch (e) { return { 왜: `${파일}: JSON 이 아닙니다 — ${e.message}` }; }
116
+ const r = 한정의(첫글(j?.이름, j?.name, 이름), j, 출처);
89
117
  return r ? { 것: r } : { 왜: `${파일}: 설명이 없습니다 (설명 · description)` };
90
118
  }
91
119
  /*
@@ -95,8 +123,20 @@ function 파일하나(경로, 파일, 출처) {
95
123
  * 이미 그 파일을 가진 사람에게 「우리 모양으로 다시 적어라」 고 하면, 그
96
124
  * 사람은 이 기능을 안 쓴다. 본문은 통째로 지침이 된다.
97
125
  */
98
- const { data, body } = frontmatter(글);
99
- const r = 한정의(data?.name ?? data?.이름 ?? 이름, { ...data, 지침: data?.지침 ?? body }, 출처);
126
+ // BOM 도 뗀다 — JSON 갈래만 떼고 있어서 BOM 붙은 .md 는 앞머리가 없는 파일로 읽혀 통째로 빠졌다(2.0.0 4회차).
127
+ const { data, body } = frontmatter(BOM떼기(글));
128
+ /*
129
+ * 본문이 비면 **앞머리에 적은 것**이 지침이 된다 (2.0.0 8회차 판정).
130
+ *
131
+ * 여태 `지침: data?.지침 ?? body` 였다. 앞머리만 적은 `.md`(본문이 빈 파일)에서 body 는
132
+ * `''` 인데, 빈 글은 nullish 가 아니라 한정의() 안의 `것?.지침 ?? 것?.prompt ?? 것?.본문`
133
+ * 이 거기서 멎었다 — 앞머리에 적어 둔 `prompt:` 가 조용히 버려지고, 화면에는 그
134
+ * 에이전트가 멀쩡히 선다. 지침 없는 하위는 아무 규칙 없이 그냥 돈다.
135
+ * 빈 것은 **없는 것으로** 넘겨 한정의() 가 나머지 이름들을 마저 보게 한다.
136
+ * 본문이 있으면 여태대로 본문이 이긴다 (머리말의 「본문은 통째로 지침이 된다」).
137
+ */
138
+ const 몸통 = 첫글(data?.지침, body) || null;
139
+ const r = 한정의(첫글(data?.name, data?.이름, 이름), { ...data, 지침: 몸통 }, 출처);
100
140
  return r ? { 것: r } : { 왜: `${파일}: 앞머리에 description 이 없습니다` };
101
141
  }
102
142
 
@@ -110,7 +150,18 @@ function 폴더하나(폴더, 출처, 모은것, 버린것) {
110
150
  if (모은것.size >= 최대) { 버린것.push(`${최대}개까지만 읽습니다 — ${d.name} 부터는 안 읽었습니다`); break; }
111
151
  try {
112
152
  const r = 파일하나(join(폴더, d.name), d.name, 출처);
113
- if (r.것) 모은것.set(r.것.이름, r.것); else 버린것.push(r.왜);
153
+ /*
154
+ * 열쇠는 **소문자**다. 찾기() 가 대소문자를 안 가리는데 여기서 가리면, 이 PC 의
155
+ * `Reviewer` 와 프로젝트의 `reviewer` 가 둘 다 서고 찾기() 는 먼저 선 이 PC 것을
156
+ * 돌려준다 — 「가까운 쪽이 이긴다」 가 글자 크기 하나로 뒤집힌다(2.0.0 4회차).
157
+ * 이름 자체는 적힌 그대로 둔다. 사람이 적은 모양으로 화면에 떠야 한다.
158
+ *
159
+ * **먼저 선 것이 이긴다.** 부르는 쪽이 프로젝트 폴더부터 읽으므로(에이전트읽기)
160
+ * 겹치면 프로젝트가 이긴다 — 여태 「나중에 읽은 것이 덮는다」 로 같은 결과를
161
+ * 냈는데, 읽는 차례가 뒤집힌 지금 덮으면 이 PC 것이 이겨 규칙이 거꾸로 선다.
162
+ */
163
+ if (!r.것) 버린것.push(r.왜);
164
+ else if (!모은것.has(r.것.이름.toLowerCase())) 모은것.set(r.것.이름.toLowerCase(), r.것);
114
165
  } catch (e) {
115
166
  버린것.push(`${d.name}: 못 읽었습니다 — ${e.message}`);
116
167
  }
@@ -120,19 +171,60 @@ function 폴더하나(폴더, 출처, 모은것, 버린것) {
120
171
  /**
121
172
  * 이 자리에서 쓸 에이전트를 다 읽는다.
122
173
  *
123
- * 이 PC 것을 먼저, 프로젝트 것을 나중에 읽는다 — 이름이 같으면 **가까운
124
- * 쪽이 이긴다.** 스킬과 같은 규칙이다(skills/discover.js 의 dedupe).
174
+ * 이름이 같으면 **가까운 쪽이 이긴다.** 스킬과 같은 규칙이다(skills/discover.js 의 dedupe).
175
+ *
176
+ * ── 프로젝트 것을 **먼저** 읽는다 (2.0.0 8회차 사냥) ────────────────────
177
+ *
178
+ * 상한(최대)은 폴더별이 아니라 **모은 수의 누계**다. 그런데 이 PC 것을 먼저 읽고 있어서,
179
+ * 집에 정의가 상한만큼 있으면 프로젝트 폴더는 첫 파일에서 바로 끊겼다 — 「이 저장소에서는
180
+ * 이렇게 검토한다」 를 적어 둔 파일이 이 PC 사정 하나로 **하나도** 안 읽힌다. 게다가 바로
181
+ * 아래 줄이 「가까운 쪽이 이긴다」 를 약속하는데, 상한이 그 약속까지 거꾸로 뒤집었다.
182
+ *
183
+ * 상한을 폴더마다 따로 두는 길도 있었지만 안 골랐다. 이 목록은 **매 요청 Task 스키마에
184
+ * 통째로 실리는 글**이라(고를말), 폴더마다 두면 실리는 양이 두 배가 된다. 상한이 있는
185
+ * 까닭 자체가 그 양이라 그것을 늘리면 안 된다.
186
+ *
187
+ * 그래서 차례를 뒤집는다 — 자리가 모자랄 때 **먼저 자리를 갖는 쪽이 지금 이 저장소 것**
188
+ * 이어야 한다. 이 PC 것은 어느 저장소에서나 쓰는 것이고, 프로젝트 것은 여기서만 쓴다.
189
+ * 여기서 안 읽히면 그 정의는 쓸 자리가 아예 없어진다. 모으는 쪽은 그래서 「먼저 선 것이
190
+ * 이긴다」 로 바뀌었다(폴더하나).
191
+ *
192
+ * ── 프로젝트 것은 **믿는 폴더에서만** 읽는다 (2.0.0 4회차 사냥) ──────────
193
+ *
194
+ * 이 파일 머리말은 「스킬과 같은 갈래라 같은 규칙으로 읽는다」 고 적어 두었다. 그런데
195
+ * 스킬이 믿는 폴더에서만 읽히게 된 뒤로 이쪽은 그 문을 안 지났다. 정의의 **설명**은
196
+ * 매 요청 Task 스키마에 실리고(고를말), **지침**은 하위 모델의 일 앞에 붙는다 —
197
+ * 둘 다 모델에게 「이렇게 하라」 고 시키는 글이고, `.deel/agents` 는 저장소에 딸려
198
+ * 온다. clone 하고 켜기만 하면 남이 적은 지시문이 스키마에 실렸다.
199
+ *
200
+ * 부르는 자리(repl·oneshot·acp·doctor)는 넷인데 문은 여기 하나에 둔다. 넷에 따로
201
+ * 두면 언젠가 한 자리가 빠진다. 안 읽었으면 `안믿음` 으로 말한다 — 제가 적은 정의가
202
+ * 왜 안 보이는지 모르는 것이 두 번째로 나쁜 일이다(hooks.js 의 안믿음 과 같은 방식).
203
+ *
204
+ * 집에서 켰으면(`root` 가 집) 프로젝트 갈래를 건너뛴다. 같은 폴더를 두 번 읽어
205
+ * 출처가 「프로젝트」 로 바뀌고, 안 믿는 판에서는 이미 읽은 것을 안 읽었다고 말한다.
125
206
  */
126
- export function 에이전트읽기(root, { 집 = homedir(), env = process.env } = {}) {
207
+ export function 에이전트읽기(root, { 집 = homedir(), env = process.env, 믿나: 믿나고르개 = null } = {}) {
127
208
  const 모은것 = new Map();
128
209
  const 버린것 = [];
129
210
  const 끔 = String(env.DEEL_AGENTS ?? '').trim().toLowerCase();
130
211
  if (끔 === 'off' || 끔 === '0' || 끔 === 'false') {
131
- return { 에이전트들: [], 버린것: [], 켜짐: false };
212
+ return { 에이전트들: [], 버린것: [], 켜짐: false, 안믿음: false };
213
+ }
214
+ let 안믿음 = false;
215
+ if (고른경로(root) !== 고른경로(집)) {
216
+ const 믿는가 = 믿나고르개 ? 믿나고르개(root) : 믿나(root, { env });
217
+ if (믿는가) {
218
+ for (const 폴더 of 자리들(root)) 폴더하나(폴더, '프로젝트', 모은것, 버린것);
219
+ } else {
220
+ // 안 읽은 것이 **있을 때만** 말한다. 없는 것(ENOENT)은 조용히, 못 읽은 것은 안 읽은 것이다.
221
+ 안믿음 = 자리들(root).some((폴더) => {
222
+ try { return readdirSync(폴더).some((n) => /\.(json|md)$/i.test(n)); } catch (e) { return e?.code !== 'ENOENT'; }
223
+ });
224
+ }
132
225
  }
133
226
  for (const 폴더 of 자리들(집)) 폴더하나(폴더, '이 PC', 모은것, 버린것);
134
- for (const 폴더 of 자리들(root)) 폴더하나(폴더, '프로젝트', 모은것, 버린것);
135
- return { 에이전트들: [...모은것.values()], 버린것, 켜짐: true };
227
+ return { 에이전트들: [...모은것.values()], 버린것, 켜짐: true, 안믿음 };
136
228
  }
137
229
 
138
230
  /** 이름으로 찾는다. 대소문자는 안 가린다 — 사람이 부르는 이름이다. */
@@ -194,6 +286,12 @@ export function 에이전트줄들(r) {
194
286
  const 줄 = [];
195
287
  if (!r.켜짐) { 줄.push({ 상태: 'warn', 이름: '에이전트', 값: '꺼져 있습니다 (DEEL_AGENTS=off)' }); return 줄; }
196
288
  for (const 왜 of r.버린것 ?? []) 줄.push({ 상태: 'warn', 이름: '에이전트 · 못 읽음', 값: 왜 });
289
+ if (r.안믿음) {
290
+ 줄.push({
291
+ 상태: 'warn', 이름: '에이전트 · 프로젝트', 값: '.deel/agents · .claude/agents',
292
+ 덧말: '믿는 폴더가 아니라서 안 읽습니다 — 읽게 하려면 deel trust',
293
+ });
294
+ }
197
295
  if (!r.에이전트들.length) return 줄;
198
296
  줄.push({
199
297
  상태: 'ok', 이름: '에이전트', 값: `${r.에이전트들.length}개`,
@@ -66,12 +66,17 @@ export function 얼마나들었나(작은것, 큰것) {
66
66
  *
67
67
  * 번호(1. 2)·글머리(- ·)·동그라미 숫자를 센다. 줄머리에 있는 것만 센다 —
68
68
  * 글 가운데 있는 "1.6.1" 같은 것을 항목으로 세면 안 된다.
69
+ *
70
+ * `\p{Mn}?` 는 ▪️ 같은 이모지 꼴 가림표(U+FE0F)다. 표에는 ▪️ 를 넣어 두었는데
71
+ * 가림표가 글머리와 빈칸 사이에 끼어 그 줄머리가 한 번도 안 맞았다 — 사용자가
72
+ * ▪️ 로 낱낱이 적어 줘도 「적어 준 것이 0개」 라, 그 목록을 그대로 되읽는 물음이
73
+ * 지나갔다. asks.js 의 줄머리는 6회차(AS3)에 고쳤는데 여기만 남아 있었다.
69
74
  */
70
75
  export function 항목수(요청) {
71
76
  const 줄들 = String(요청 ?? '').split('\n');
72
77
  let 몇 = 0;
73
78
  for (const 줄 of 줄들) {
74
- if (/^\s*(\d{1,2}\s*[.)]|[-*·▪️•]|[①-⑮])\s+\S/.test(줄)) 몇 += 1;
79
+ if (/^\s*(\d{1,2}\s*[.)]|[-*·▪️•]|[①-⑮])\p{Mn}?\s+\S/u.test(줄)) 몇 += 1;
75
80
  }
76
81
  return 몇;
77
82
  }
@@ -121,7 +126,10 @@ const 고르라는물음 = /어(느|떤)\s*(것|거|쪽|부분|순서)|무엇(
121
126
  *
122
127
  * '어느 쪽으로 할까요' 는 여기 안 넣는다. 그건 진짜 갈림길일 때가 많다.
123
128
  */
124
- const 무슨일할까 = /(어떤|무슨)\s*(작업|일)(을|를)?\s*(진행|할|하)|무엇을\s*(할|진행|해\s*드릴)|뭘\s*(할|해\s*드릴)|어떤\s*것부터|무엇부터|뭐부터|what\s+(should|would)\s+(i|we)\s+(do|work\s+on)|how\s+(should|would)\s+(i|we)\s+proceed/i;
129
+ // 사람의 말이 갈림길을 묻는 꼴인가 — 물음표로 끝나거나 「쓸까·할까」 「or·vs·아니면」.
130
+ const 갈림길물음 = /[??]\s*$|(?:할까|쓸까|좋을까|나을까|갈까|둘까)|\b(?:or|vs\.?|versus)\b|아니면/i;
131
+
132
+ const 무슨일할까 =/(어떤|무슨)\s*(작업|일)(을|를)?\s*(진행|할|하)|무엇을\s*(할|진행|해\s*드릴)|뭘\s*(할|해\s*드릴)|어떤\s*것부터|무엇부터|뭐부터|what\s+(should|would)\s+(i|we)\s+(do|work\s+on)|how\s+(should|would)\s+(i|we)\s+proceed/i;
125
133
 
126
134
  /*
127
135
  * 이만큼 겹치면 "이미 물어본 것" 으로 본다.
@@ -199,6 +207,24 @@ export function 물음검사({ 물음 = '', 고를것 = [], 이해 = '', 요청
199
207
  // 선택지 하나하나가 사용자 말 안에 이미 들어 있으면, 그건 갈림길이 아니라
200
208
  // 목록을 되읽어 준 것이다. 둘 이상이면 그렇게 본다 — 하나는 우연일 수 있다.
201
209
  const 이미있는것 = 것들.filter((x) => 얼마나들었나(x, 요청) >= 같은것문턱);
210
+ /*
211
+ * 단, 사람이 그 둘을 **갈림길로 물었으면** 할 일 목록이 아니다.
212
+ *
213
+ * 「PostgreSQL 쓸까 SQLite 쓸까?」 에 모델이 그 둘을 선택지로 되물었더니 「할 일
214
+ * 목록이니 **전부** 하세요」 가 돌아갔다 — 둘 다 쓸 수는 없다. 되묻는 것을 막는
215
+ * 것은 같다. 돌려주는 말만 다르다: 사람은 권해 달라고 물은 것이다.
216
+ * 고치라는 말이 들었거나(「둘 다 고쳐줄래?」) 줄머리 목록이면 여전히 할 일 목록이다.
217
+ * (6회차 Gemini 되묻기6 Q2)
218
+ */
219
+ if (이미있는것.length >= 2 && 항목수(요청) < 2 && !손대라했나(요청) && 갈림길물음.test(요청)) {
220
+ return {
221
+ ok: false,
222
+ 왜: `사람이 물은 갈림길(${이미있는것.slice(0, 3).join(' · ')})을 그대로 되물음`,
223
+ 할말: '사람은 이 가운데 무엇이 나은지 **물었습니다**. 같은 물음을 돌려주지 마세요.'
224
+ + ' 지금 아는 것(코드·설정·쓰임새)으로 견줘서 하나를 권하고, 까닭을 한두 줄 적으세요.'
225
+ + ' 사람만 아는 것 하나가 갈림길을 가르면(예: 어디에 배포하나) 그 한 가지만 콕 집어 물으세요.',
226
+ };
227
+ }
202
228
  if (이미있는것.length >= 2) {
203
229
  return {
204
230
  ok: false,
@@ -240,9 +266,21 @@ export function 물음검사({ 물음 = '', 고를것 = [], 이해 = '', 요청
240
266
  // 사람 눈에는 앞의 대답이 통째로 무시된 것으로 보인다 — 실제로도 그렇다.
241
267
  const 앞것 = (Array.isArray(이미물은것) ? 이미물은것 : []);
242
268
  for (const 앞 of 앞것) {
243
- const 앞물음 = String(앞?.물음 ?? 앞 ?? '');
269
+ // 글자로 적힌 앞것도 받는다. 다만 **객체인데 물음이 없으면** 건너뛴다 —
270
+ // `앞?.물음 ?? 앞` 으로 받았더니 그 객체가 "[object Object]" 로 굳어,
271
+ // 엉뚱한 물음이 그 글자와 겹쳐 막히고 막는 말에도 그대로 실려 나갔다.
272
+ const 앞물음 = typeof 앞 === 'string' ? 앞 : String(앞?.물음 ?? '');
244
273
  if (!앞물음) continue;
245
- if (얼마나들었나(물, 앞물음) < 또물음문턱) continue;
274
+ /*
275
+ * 새 물음이 앞물음 안에 얼마나 드나만 재면, 앞에 서두를 붙인 같은 물음이 지나간다 —
276
+ * 「답변을 확인하지 못해 다시 여쭙는데 어떤 작업을 진행할까요?」 가 0.38 이었다.
277
+ * 「무슨 일을 할까요」 꼴끼리는 거꾸로(앞물음이 새 물음 안에 드나)도 본다. 이 꼴에만
278
+ * 거는 것은, 「어느 것으로 할까요?」 같은 짧은 물음은 **좁힌** 새 물음 안에도 통째로
279
+ * 들기 때문이다 — 그건 또 묻는 것이 아니다. (6회차 Gemini 되묻기6 Q3)
280
+ */
281
+ const 겹침 = Math.max(얼마나들었나(물, 앞물음),
282
+ 무슨일할까.test(앞물음) && 무슨일할까.test(물) ? 얼마나들었나(앞물음, 물) : 0);
283
+ if (겹침 < 또물음문턱) continue;
246
284
  const 답 = String(앞?.답 ?? '').trim();
247
285
  return {
248
286
  ok: false,
@@ -266,8 +304,14 @@ export function 물음검사({ 물음 = '', 고를것 = [], 이해 = '', 요청
266
304
  }
267
305
 
268
306
  // ── 5. 한 번에 하나만 ────────────────────────────────────────────────
269
- const 물음표 = (물.match(/[??]/g) ?? []).length;
270
- if (물음표 >= 2) {
307
+ // 코드 울타리·코드 조각·주소 속 물음표는 물음이 아니다 — 「`user?.name ? …` 로 수정할까요?」
308
+ // 는 물음 하나인데 셋으로 셌다 (6회차 Gemini 되묻기6 Q1).
309
+ const 물음글 = 물.replace(/```[\s\S]*?(?:```|$)/g, ' ').replace(/`[^`\n]*`/g, ' ').replace(/\b[a-z][a-z0-9+.-]*:\/\/\S+/gi, ' ');
310
+ const 물음표 = (물음글.match(/[??]/g) ?? []).length;
311
+ // 잇따라 찍은 물음표(「할까요??」)는 강조지 물음이 아니다 — 겹쳐 찍은 만큼 도로 뺀다.
312
+ // 안 빼면 물음 하나가 둘로 세어져 「한 번에 여럿을 물음」 으로 막힌다.
313
+ const 겹쳐찍은것 = (물음글.match(/[??]{2,}/g) ?? []).reduce((합, 무리) => 합 + 무리.length - 1, 0);
314
+ if (물음표 - 겹쳐찍은것 >= 2) {
271
315
  return {
272
316
  ok: false,
273
317
  왜: '한 번에 여럿을 물음',
package/src/agent/asks.js CHANGED
@@ -75,7 +75,27 @@ function 낱말이닿았나(낱말, 덩어리) {
75
75
  *
76
76
  * 영문은 안 줄인다. cli·deel·npm 처럼 원래 짧아서, 줄이면 아무 데나 걸린다.
77
77
  */
78
- if (!/^[가-힣]/.test(낱말)) return false;
78
+ if (!/^[가-힣]/.test(낱말)) {
79
+ /*
80
+ * 영문·숫자 머리에 한글 조사가 붙은 낱말(`readme를` · `api를` · `3번을`)은 **꼬리만** 뗀다
81
+ * (6회차 시킨것6au-a AS2). 낱말을 가르는 자리가 영문과 한글 사이를 안 끊어서 조사가 붙은
82
+ * 채로 남았고, 위의 줄이기는 한글로 시작할 때만 해서 README.md 를 고치고 「README 고쳤습니다」
83
+ * 라고 보고해도 안 닿았다 — 다 한 일을 안 했다고 되밀었다. 영문 머리 자체는 여전히 안 줄인다.
84
+ */
85
+ const 머리 = /^[0-9a-z]+(?=[가-힣]+$)/.exec(낱말)?.[0];
86
+ if (!머리) return false;
87
+ /*
88
+ * 머리가 **한 글자**면(`7을` · `3을`) 아래 고리가 한 번도 안 돈다 — 문턱이 두
89
+ * 글자여서 시작값이 이미 문턱 밑이다. 「이슈 7을 닫아줘」 를 닫고 「이슈 7
90
+ * 닫았습니다」 라고 적어도 안 닿아, 한 일을 안 했다고 되밀었다. 여기서 머리까지
91
+ * 내려가 본다 — 꼬리만 떼자고 한 자리이니 머리는 그대로 남는다.
92
+ */
93
+ if (머리.length < 2 && 덩어리.includes(머리)) return true;
94
+ for (let n = 낱말.length - 1; n >= Math.max(머리.length, 2); n--) {
95
+ if (덩어리.includes(낱말.slice(0, n))) return true;
96
+ }
97
+ return false;
98
+ }
79
99
  for (let n = 낱말.length - 1; n >= 2; n--) {
80
100
  if (덩어리.includes(낱말.slice(0, n))) return true;
81
101
  }
@@ -91,7 +111,9 @@ function 낱말이닿았나(낱말, 덩어리) {
91
111
  const 시킴꼴 = /(줘|주세요|주라|해라|하라|하자|해다오|할래|해야\s*(해|돼|한다)|바랍니다|부탁(해|합니다|드립니다))[\s.!~]*$/;
92
112
 
93
113
  // 줄머리 표. 번호(1. 2))·글머리(- * ·)·동그라미 숫자.
94
- const 줄머리 = /^\s*(?:(\d{1,2})\s*[.)]|[-*·▪️•]|[①-⑮])\s+/;
114
+ const 줄머리 = /^\s*(?:(\d{1,2})\s*[.)]|[-*·▪️•]|[①-⑮])\p{Mn}?\s+/u;
115
+ // 끝의 \p{Mn}? 는 ▪️ 같은 이모지 꼴 가림표(U+FE0F)다. 표에는 ▪️ 를 넣어 두었는데 가림표가
116
+ // 빈칸 앞에 끼어 그 줄머리가 한 번도 안 맞았다 (6회차 시킨것6au-a AS3).
95
117
 
96
118
  /*
97
119
  * ── 시킨 것이 아니라 **조건**인 줄 ─────────────────────────────────────
@@ -135,15 +157,47 @@ const 맨동사 = new Set([
135
157
  ]);
136
158
  // 「…하지 마」 는 무슨 일이 있어도 할 일이 아니다. 안 하는 것이 다 한 것이다.
137
159
  const 하지마 = /(?:하지|치지|시키지|되지|넣지|쓰지|고치지|바꾸지|건드리지|만지지)\s*(?:는)?\s*(?:마|말)/;
138
- const 하지마En = /\b(?:do not|don'?t|never|without)\b/i;
160
+ // `without` 은 여기 없다. 그건 금지가 아니라 **단서**다 — 「Deploy the app without
161
+ // downtime」 은 하라는 말인데 줄째로 조건이 되어 빠졌고, 남은 항목이 하나뿐이면
162
+ // 대조가 통째로 안 돈다(요청항목들 은 둘부터 센다).
163
+ const 하지마En = /\b(?:do not|don'?t|never)\b/i;
164
+
165
+ /*
166
+ * 줄 **끝**이 「…지 마」 꼴이면 그 줄은 통째로 금지다 (6회차 시킨것6au-b AS6).
167
+ *
168
+ * 위 동사 목록은 줄 가운데 「고치지 말고 …」 를 잡는 자리라 동사를 골라 두었다 —
169
+ * 「잊지 말고 테스트 돌려줘」 는 금지가 아니라 할 일이다. 그런데 그 목록 밖의 「손대지 마」 ·
170
+ * 「건들지 마세요」 · 「지우지 말 것」 이 할 일로 잡혀, 지킨 금지를 「지금 하세요」 로 되밀었다.
171
+ * 줄 끝에 오면 뒤에 이어지는 할 일이 없으니 동사를 가릴 까닭이 없다.
172
+ */
173
+ const 줄끝금지 = /지\s*(?:마|마세요|마라|말\s*것|말아\s*(?:줘|주세요|라)?)[\s.!~]*$/;
174
+
175
+ /** 조건에만 쓰이는 말인가 — 수·한계말·맨 동사. 이것뿐이면 그 줄은 할 일이 아니다. */
176
+ const 조건말인가 = (w) => 한계말.has(w) || 맨동사.has(w) || /^\d/.test(w);
139
177
 
140
178
  /** 이 줄이 할 일이 아니라 그 일에 붙은 조건인가. */
141
179
  export function 조건줄인가(글) {
142
180
  const s = String(글 ?? '');
143
- if (하지마.test(s) || 하지마En.test(s)) return true;
181
+ /*
182
+ * 줄 **가운데** 금지는 그 앞까지만 금지다. 뒤에 붙은 것은 할 일이다 —
183
+ * 「기존 스타일은 고치지 말고 새 테마 파일을 만들어줘」 를 통째로 금지로 보면
184
+ * 「만들어줘」 가 사라지고, 그 줄이 빠지면서 항목이 둘 미만이 되면 대조가
185
+ * 통째로 안 돈다(요청항목들 은 둘부터 센다).
186
+ *
187
+ * 그래서 금지 뒤에 **남는 말**로 가른다. 남는 것이 조건말뿐이거나 아예 없으면
188
+ * (「기존 코드는 고치지 말고.」 · 「아무것도 건드리지 말고 그냥 둬」) 줄 전체가 금지다.
189
+ *
190
+ * 줄 끝이 금지인 줄은 아예 안 본다. 「코드는 건드리지 마세요」 의 남는 말은 '세요'
191
+ * 인데, 그걸 뒤에 붙은 할 일로 읽으면 지킨 금지가 할 일로 되살아난다.
192
+ */
193
+ if (!줄끝금지.test(s)) {
194
+ const 금지 = 하지마.exec(s);
195
+ if (금지 && 알맹이낱말(s.slice(금지.index + 금지[0].length)).some((w) => !조건말인가(w))) return false;
196
+ }
197
+ if (하지마.test(s) || 하지마En.test(s) || 줄끝금지.test(s)) return true;
144
198
  const 낱말 = 알맹이낱말(s);
145
199
  if (!낱말.length) return false; // 잴 것이 없으면 안 건다 — 빠진것 이 따로 거른다
146
- return 낱말.every((w) => 한계말.has(w) || 맨동사.has(w) || /^\d/.test(w));
200
+ return 낱말.every(조건말인가);
147
201
  }
148
202
 
149
203
  /**
@@ -141,9 +141,16 @@ export function 한번에낼글자수(ctx) {
141
141
  * 아니다. 부모만큼 주면 하위가 부모 일을 통째로 다시 하려 들고, 그러면
142
142
  * 창을 나눈 뜻이 없어진다. 대신 너무 적으면 맡긴 덩이도 못 끝내니
143
143
  * 8걸음은 무슨 일이 있어도 준다.
144
+ *
145
+ * 부모 모드를 알면 하위 모드와 부모 모드 가운데 걸음이 적은 쪽의 절반이다.
146
+ * 하위 모드만 보면, 걸음이 두 배인 모드(orchestrator)를 하위로 고른 순간
147
+ * 그 절반이 곧 부모 몫이 되어 「부모보다 적게」 가 깨졌다 — code 부모가
148
+ * orchestrator 하위를 띄우면 16k 에서 24 대 24. 모드를 부모보다 세게 못
149
+ * 올리는 것(tools/task.js 하위모드)과 같은 뜻을 걸음 수에도 건다.
144
150
  */
145
- export function 하위걸음수(모드id, ctx) {
146
- return Math.max(8, Math.round(걸음수(모드id, ctx) / 2));
151
+ export function 하위걸음수(모드id, ctx, 부모모드id = null) {
152
+ const 바탕 = 부모모드id == null ? 걸음수(모드id, ctx) : Math.min(걸음수(모드id, ctx), 걸음수(부모모드id, ctx));
153
+ return Math.max(8, Math.round(바탕 / 2));
147
154
  }
148
155
 
149
156
  /**
package/src/agent/card.js CHANGED
@@ -28,7 +28,8 @@
28
28
  //
29
29
  // 조금 겪고 함부로 바꾸지 않는다. 걸음이 얼마 안 될 때 우연히 한 번 잘린 것으로
30
30
  // 하네스를 조이면, 멀쩡한 모델을 붙들어 매는 셈이 된다 — 안 배우느니만 못하다.
31
- // 그래서 걸음 상한과 비율 문턱을 둘 다 넘어야 움직인다.
31
+ // 그래서 걸음 하한(최소걸음)과 비율 문턱을 둘 다 넘어야 움직인다. 많이 겪을수록
32
+ // 더 잘 맞추는 쪽이지, 많이 겪으면 멈추는 쪽이 아니다.
32
33
 
33
34
  import { 내장카드, 추론형왜 } from './preset.js';
34
35
 
@@ -122,7 +123,12 @@ export function 카드(모델, 본것 = null, 겪은것 = null) {
122
123
  내장,
123
124
  // 판단을 아직 못 하는 상태인지 화면이 알아야 한다 — '바꾼 것 없음' 과
124
125
  // '아직 모름' 은 다른 말이다.
125
- 아직모름: 걸음 < 최소걸음,
126
+ //
127
+ // **이미 바꾼 것이 있으면 미루고 있는 것이 아니다.** 내장 카드(preset.js)는
128
+ // 걸음 관문 앞에 서서 한 걸음도 안 걸은 모델의 하네스를 이미 바꾼다. 그런데
129
+ // /card 화면은 아직모름 이면 왜 를 통째로 감추고 「아직 판단하지 않습니다」
130
+ // 만 찍는다(commands/work.js) — 바꿔 놓고 안 바꿨다고 말하는 화면이 된다.
131
+ 아직모름: 걸음 < 최소걸음 && 왜.length === 0,
126
132
  };
127
133
  }
128
134