cuelo 0.10.49 → 0.10.50

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 (63) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +12 -12
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/prerender-manifest.json +3 -3
  5. package/.next/required-server-files.js +1 -1
  6. package/.next/required-server-files.json +1 -1
  7. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  8. package/.next/server/app/_global-error.html +1 -1
  9. package/.next/server/app/_global-error.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  17. package/.next/server/app/_not-found.html +1 -1
  18. package/.next/server/app/_not-found.rsc +2 -2
  19. package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  20. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  21. package/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  22. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  23. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  24. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  25. package/.next/server/app/api/updates/route.js +1 -1
  26. package/.next/server/app/index.html +1 -1
  27. package/.next/server/app/index.rsc +4 -4
  28. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  29. package/.next/server/app/index.segments/_full.segment.rsc +4 -4
  30. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  31. package/.next/server/app/index.segments/_index.segment.rsc +2 -2
  32. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  33. package/.next/server/app/page.js +2 -2
  34. package/.next/server/app/page_client-reference-manifest.js +1 -1
  35. package/.next/server/app/recover/page_client-reference-manifest.js +1 -1
  36. package/.next/server/app/recover.html +1 -1
  37. package/.next/server/app/recover.rsc +2 -2
  38. package/.next/server/app/recover.segments/_full.segment.rsc +2 -2
  39. package/.next/server/app/recover.segments/_head.segment.rsc +1 -1
  40. package/.next/server/app/recover.segments/_index.segment.rsc +2 -2
  41. package/.next/server/app/recover.segments/_tree.segment.rsc +1 -1
  42. package/.next/server/app/recover.segments/recover/__PAGE__.segment.rsc +1 -1
  43. package/.next/server/app/recover.segments/recover.segment.rsc +1 -1
  44. package/.next/server/app-paths-manifest.json +12 -12
  45. package/.next/server/middleware-build-manifest.js +1 -1
  46. package/.next/server/pages/404.html +1 -1
  47. package/.next/server/pages/500.html +1 -1
  48. package/.next/server/server-reference-manifest.json +1 -1
  49. package/.next/static/chunks/app/{layout-9489cd70db38d552.js → layout-56f0d314d8b60841.js} +1 -1
  50. package/.next/static/chunks/app/{page-c7c715cf078c3418.js → page-cf9f8e3f4da25f48.js} +2 -2
  51. package/.next/trace +107 -107
  52. package/.next/trace-build +1 -1
  53. package/Tools/CUELO_Setup/files/native-runtime-patch.js +1 -1
  54. package/Tools/OMP_Global_Config/agent/agents/maker.md +2 -2
  55. package/Tools/OMP_Global_Config/agent/rules/harness-policy.json +1 -1
  56. package/Tools/OMP_Global_Config/agent/rules/meta-harness.md +1 -1
  57. package/Tools/OMP_Global_Config/agent/sop/maker.md +1 -1
  58. package/Tools/OMP_Global_Config/agent/tools/windows-control/control.ps1 +200 -0
  59. package/Tools/OMP_Global_Config/agent/tools/windows-control/index.ts +255 -0
  60. package/Tools/OMP_Global_Config/patches/apply-core-patch.mjs +9 -0
  61. package/package.json +1 -1
  62. /package/.next/static/{5gOfHmA_lDytew7Oyx7wS → U-qlrriaOFPjcylXwOLSr}/_buildManifest.js +0 -0
  63. /package/.next/static/{5gOfHmA_lDytew7Oyx7wS → U-qlrriaOFPjcylXwOLSr}/_ssgManifest.js +0 -0
package/.next/trace-build CHANGED
@@ -1 +1 @@
1
- [{"name":"run-webpack","duration":77684283,"timestamp":619091,"id":14,"parentId":1,"tags":{},"startTime":1792536213341,"traceId":"10062087fa58dc8a"},{"name":"run-typescript","duration":13674772,"timestamp":78682976,"id":10997,"parentId":1,"tags":{},"startTime":1792536291406,"traceId":"10062087fa58dc8a"},{"name":"static-check","duration":4262935,"timestamp":92449601,"id":11000,"parentId":1,"tags":{},"startTime":1792536305173,"traceId":"10062087fa58dc8a"},{"name":"static-generation","duration":7399136,"timestamp":96788483,"id":11172,"parentId":1,"tags":{},"startTime":1792536309512,"traceId":"10062087fa58dc8a"},{"name":"collect-build-traces","duration":15441300,"timestamp":96772023,"id":11169,"parentId":1,"tags":{},"startTime":1792536309495,"traceId":"10062087fa58dc8a"},{"name":"telemetry-flush","duration":46,"timestamp":112216686,"id":11181,"parentId":1,"tags":{},"startTime":1792536324940,"traceId":"10062087fa58dc8a"},{"name":"next-build","duration":111682196,"timestamp":534554,"id":1,"tags":{"buildMode":"default","version":"16.2.12","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1792536213256,"traceId":"10062087fa58dc8a"}]
1
+ [{"name":"run-webpack","duration":111128781,"timestamp":842435,"id":14,"parentId":1,"tags":{},"startTime":1792539390956,"traceId":"5549c2f2c6ebad40"},{"name":"run-typescript","duration":20400885,"timestamp":112327456,"id":10997,"parentId":1,"tags":{},"startTime":1792539502442,"traceId":"5549c2f2c6ebad40"},{"name":"static-check","duration":4411852,"timestamp":132826944,"id":11000,"parentId":1,"tags":{},"startTime":1792539522941,"traceId":"5549c2f2c6ebad40"},{"name":"static-generation","duration":14142279,"timestamp":137350537,"id":11172,"parentId":1,"tags":{},"startTime":1792539527465,"traceId":"5549c2f2c6ebad40"},{"name":"collect-build-traces","duration":23147676,"timestamp":137312713,"id":11169,"parentId":1,"tags":{},"startTime":1792539527427,"traceId":"5549c2f2c6ebad40"},{"name":"telemetry-flush","duration":141,"timestamp":160465027,"id":11181,"parentId":1,"tags":{},"startTime":1792539550579,"traceId":"5549c2f2c6ebad40"},{"name":"next-build","duration":159745637,"timestamp":719553,"id":1,"tags":{"buildMode":"default","version":"16.2.12","bundler":"webpack","has-custom-webpack-config":"true","use-build-worker":"false"},"startTime":1792539390834,"traceId":"5549c2f2c6ebad40"}]
@@ -9,7 +9,7 @@ const MEMORY_RE = /(const phase1Model = await resolveMemoryModel\(\{[^}]*?fallba
9
9
  const MEMORY_DONE = /const phase1Model = await resolveMemoryModel\(\{[^}]*?fallbackRole: "smol"/;
10
10
  const EXPECTED = {
11
11
  name: 'cuelo',
12
- version: '0.10.49',
12
+ version: '0.10.50',
13
13
  distribution: 'cuelo',
14
14
  coreVersion: '18.12.3',
15
15
  };
@@ -3,10 +3,10 @@ name: maker
3
3
  description: Delegated end-to-end implementation agent that investigates, edits, repairs, validates its own slice, and verifies the actual surface it changed.
4
4
  model: "@implSonnet"
5
5
  thinking-level: auto
6
- tools: [read, bash, edit, write, grep, glob, skim, draft, lsp, eval, generate_image, ast_grep, ast_edit, debug, todo, web_search, windows_build, checkpoint]
6
+ tools: [read, bash, edit, write, grep, glob, skim, draft, lsp, eval, generate_image, ast_grep, ast_edit, debug, todo, web_search, windows_build, windows_control, checkpoint]
7
7
  ---
8
8
 
9
- <!-- omp-global-config:generated source-hash=3357fab21461
9
+ <!-- omp-global-config:generated source-hash=cf0f7c8e6905
10
10
  이 파일은 patches/build-agents.mjs 가 만든 빌드 산출물이다. 직접 수정하지 마라.
11
11
  고칠 곳: agent/sop/maker.md · agent/sop/_writer.md · agent/sop/_common.md
12
12
  재생성: node patches/build-agents.mjs 검사: node patches/build-agents.mjs --check
@@ -851,7 +851,7 @@
851
851
  "codeAndPolicyReviewOwner": "main",
852
852
  "designJudgment": "main-or-the-owning-maker-reads-the-design-rules-and-skills-there-is-no-separate-design-role",
853
853
  "repositoryAccess": "owner-writable-inside-its-brief-boundary-only",
854
- "tools": ["read", "bash", "edit", "write", "grep", "glob", "skim", "draft", "lsp", "eval", "generate_image", "ast_grep", "ast_edit", "debug", "todo", "web_search", "windows_build", "checkpoint"],
854
+ "tools": ["read", "bash", "edit", "write", "grep", "glob", "skim", "draft", "lsp", "eval", "generate_image", "ast_grep", "ast_edit", "debug", "todo", "web_search", "windows_build", "windows_control", "checkpoint"],
855
855
  "runtimeAutoPair": "checkpoint-in-frontmatter-causes-core-createTools-to-add-rewind-to-the-restricted-session-tool-surface-rewind-is-intentionally-absent-from-frontmatter",
856
856
  "execution": "async",
857
857
  "broadValidation": "the-owner-runs-the-ordinary-project-checks-its-slice-needs-and-skips-unrelated-project-wide-suites",
@@ -14,7 +14,7 @@ globs: ["Tools/OMP_Global_Config/**"]
14
14
  발주 때 Main이 고른 `tasks[].model`의 `:auto` selector가 child auto를 켜고, 실제 단계는 그 모델의 전체 범위에서 child가 고른다.
15
15
  모델·fallback은 `modelRoles`, 모델 분류·추론은 `routing.modelSelection`·`routing.effortSelection`이 정한다.
16
16
  - frontmatter 도구는 `read, bash, edit, write, grep, glob, skim, draft, lsp, eval,
17
- generate_image, ast_grep, ast_edit, debug, todo, web_search, windows_build, checkpoint`다. core 18.3.0에는 `hub`가
17
+ generate_image, ast_grep, ast_edit, debug, todo, web_search, windows_build, windows_control, checkpoint`다. core 18.3.0에는 `hub`가
18
18
  없고(목록에 남겨도 조용히 버려진다) `wait`는 top-level 전용이라 Maker(depth 1)에 넣어도 생기지 않으므로 둘 다 적지 않는다.
19
19
  restricted session에서 `checkpoint`를 명시하면 core `createTools`가 `rewind`를 안전 쌍으로
20
20
  자동 추가하므로 frontmatter에는 `rewind`를 중복하지 않지만 실제 runtime에는 둘 다 노출된다.
@@ -2,7 +2,7 @@
2
2
  name: maker
3
3
  description: Delegated end-to-end implementation agent that investigates, edits, repairs, validates its own slice, and verifies the actual surface it changed.
4
4
  model: "@implSonnet"
5
- tools: [read, bash, edit, write, grep, glob, skim, draft, lsp, eval, generate_image, ast_grep, ast_edit, debug, todo, web_search, windows_build, checkpoint]
5
+ tools: [read, bash, edit, write, grep, glob, skim, draft, lsp, eval, generate_image, ast_grep, ast_edit, debug, todo, web_search, windows_build, windows_control, checkpoint]
6
6
  ---
7
7
 
8
8
  You are the delegated implementation Maker on the default `modelRoles.implSonnet` slot. A delegated slice
@@ -0,0 +1,200 @@
1
+ param([Parameter(Mandatory = $true)][string]$Request)
2
+ # windows_control 도구의 Windows 쪽 본체. 요청은 base64(UTF-8 JSON) 한 인자로 받는다(WSL 인자 따옴표 문제를 피한다).
3
+ # 클래식 Win32 컨트롤(WinForms·VB6·MFC)의 자식 창에 SendMessageTimeout으로 WM_SETTEXT·BM_CLICK을 보낸다.
4
+ # 전면 창·포커스는 바꾸지 않는다(SetForegroundWindow·SetFocus·SendInput을 쓰지 않는다).
5
+ [Console]::OutputEncoding = [Text.UTF8Encoding]::new($false)
6
+ $ErrorActionPreference = 'Stop'
7
+
8
+ Add-Type -TypeDefinition @"
9
+ using System;
10
+ using System.Text;
11
+ using System.Collections.Generic;
12
+ using System.Runtime.InteropServices;
13
+
14
+ public class CueloChild {
15
+ public long Hwnd;
16
+ public string ClassName;
17
+ public string Text;
18
+ public int ControlId;
19
+ public bool Visible;
20
+ public bool Enabled;
21
+ public int Check;
22
+ }
23
+
24
+ public static class CueloWin {
25
+ private delegate bool EnumProc(IntPtr h, IntPtr l);
26
+ [DllImport("user32.dll")] private static extern bool EnumChildWindows(IntPtr p, EnumProc f, IntPtr l);
27
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetClassName(IntPtr h, StringBuilder s, int n);
28
+ [DllImport("user32.dll")] private static extern int GetDlgCtrlID(IntPtr h);
29
+ [DllImport("user32.dll")] private static extern bool IsWindowVisible(IntPtr h);
30
+ [DllImport("user32.dll")] private static extern bool IsWindowEnabled(IntPtr h);
31
+ [DllImport("user32.dll")] private static extern bool IsWindow(IntPtr h);
32
+ [DllImport("user32.dll")] private static extern int GetWindowLong(IntPtr h, int index);
33
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
34
+ [DllImport("user32.dll", SetLastError = true)]
35
+ private static extern IntPtr SendMessageTimeout(IntPtr h, uint m, IntPtr w, IntPtr l, uint flags, uint timeout, out IntPtr result);
36
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SendMessageTimeoutW")]
37
+ private static extern IntPtr SendMessageTimeoutText(IntPtr h, uint m, IntPtr w, StringBuilder l, uint flags, uint timeout, out IntPtr result);
38
+ [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode, EntryPoint = "SendMessageTimeoutW")]
39
+ private static extern IntPtr SendMessageTimeoutString(IntPtr h, uint m, IntPtr w, string l, uint flags, uint timeout, out IntPtr result);
40
+
41
+ private const uint SMTO_ABORTIFHUNG = 2;
42
+ private const uint WM_SETTEXT = 0x000C, WM_GETTEXT = 0x000D, WM_GETTEXTLENGTH = 0x000E;
43
+ private const uint BM_GETCHECK = 0x00F0, BM_CLICK = 0x00F5;
44
+ private const int GWL_STYLE = -16;
45
+
46
+ public static bool Exists(long hwnd) { return IsWindow(new IntPtr(hwnd)); }
47
+
48
+ public static string ReadText(IntPtr h) {
49
+ IntPtr length;
50
+ if (SendMessageTimeout(h, WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero, SMTO_ABORTIFHUNG, 1000, out length) == IntPtr.Zero) return null;
51
+ int n = (int)length.ToInt64();
52
+ if (n < 0) n = 0;
53
+ if (n > 65535) n = 65535;
54
+ StringBuilder buffer = new StringBuilder(n + 1);
55
+ IntPtr copied;
56
+ if (SendMessageTimeoutText(h, WM_GETTEXT, new IntPtr(n + 1), buffer, SMTO_ABORTIFHUNG, 1000, out copied) == IntPtr.Zero) return null;
57
+ return buffer.ToString();
58
+ }
59
+
60
+ // Check state only for check-box and radio-button styles; push buttons report -1.
61
+ private static int CheckState(IntPtr h, string className) {
62
+ if (className.IndexOf("BUTTON", StringComparison.OrdinalIgnoreCase) < 0) return -1;
63
+ int kind = GetWindowLong(h, GWL_STYLE) & 0xF;
64
+ if (kind != 2 && kind != 3 && kind != 4 && kind != 5 && kind != 6 && kind != 9) return -1;
65
+ IntPtr state;
66
+ if (SendMessageTimeout(h, BM_GETCHECK, IntPtr.Zero, IntPtr.Zero, SMTO_ABORTIFHUNG, 1000, out state) == IntPtr.Zero) return -1;
67
+ return (int)state.ToInt64();
68
+ }
69
+
70
+ public static CueloChild Describe(IntPtr h) {
71
+ StringBuilder cls = new StringBuilder(256);
72
+ GetClassName(h, cls, 256);
73
+ CueloChild child = new CueloChild();
74
+ child.Hwnd = h.ToInt64();
75
+ child.ClassName = cls.ToString();
76
+ child.Text = ReadText(h);
77
+ child.ControlId = GetDlgCtrlID(h);
78
+ child.Visible = IsWindowVisible(h);
79
+ child.Enabled = IsWindowEnabled(h);
80
+ child.Check = CheckState(h, child.ClassName);
81
+ return child;
82
+ }
83
+
84
+ public static List<CueloChild> Children(long top) {
85
+ List<CueloChild> list = new List<CueloChild>();
86
+ EnumChildWindows(new IntPtr(top), delegate (IntPtr h, IntPtr l) { list.Add(Describe(h)); return true; }, IntPtr.Zero);
87
+ return list;
88
+ }
89
+
90
+ // Returns 0 when delivered; otherwise the Win32 error (1460 = timeout, e.g. a modal dialog is still open; 5 = elevated target).
91
+ public static int SetText(long hwnd, string value) {
92
+ IntPtr result;
93
+ if (SendMessageTimeoutString(new IntPtr(hwnd), WM_SETTEXT, IntPtr.Zero, value, SMTO_ABORTIFHUNG, 2000, out result) != IntPtr.Zero) return 0;
94
+ int error = Marshal.GetLastWin32Error();
95
+ return error == 0 ? -1 : error;
96
+ }
97
+
98
+ public static int Click(long hwnd, uint timeoutMs) {
99
+ IntPtr result;
100
+ if (SendMessageTimeout(new IntPtr(hwnd), BM_CLICK, IntPtr.Zero, IntPtr.Zero, SMTO_ABORTIFHUNG, timeoutMs, out result) != IntPtr.Zero) return 0;
101
+ int error = Marshal.GetLastWin32Error();
102
+ return error == 0 ? -1 : error;
103
+ }
104
+ }
105
+ "@
106
+
107
+ function Write-Result($value) { $value | ConvertTo-Json -Depth 5 -Compress }
108
+
109
+ function Get-Changes($before, $after) {
110
+ $old = @{}
111
+ foreach ($child in $before) { $old[$child.Hwnd] = $child }
112
+ $changes = @()
113
+ foreach ($child in $after) {
114
+ $prev = $old[$child.Hwnd]
115
+ if ($null -eq $prev) {
116
+ $changes += [ordered]@{ hwnd = $child.Hwnd; className = $child.ClassName; added = $true; text = $child.Text }
117
+ continue
118
+ }
119
+ if ($prev.Text -ne $child.Text -or $prev.Check -ne $child.Check -or $prev.Enabled -ne $child.Enabled -or $prev.Visible -ne $child.Visible) {
120
+ $changes += [ordered]@{
121
+ hwnd = $child.Hwnd; className = $child.ClassName
122
+ textBefore = $prev.Text; textAfter = $child.Text
123
+ checkBefore = $prev.Check; checkAfter = $child.Check
124
+ enabledAfter = $child.Enabled; visibleAfter = $child.Visible
125
+ }
126
+ }
127
+ }
128
+ # 사라진 컨트롤도 변화다(창은 남았는데 버튼이 자식을 없앤 경우).
129
+ $now = @{}
130
+ foreach ($child in $after) { $now[$child.Hwnd] = $true }
131
+ foreach ($child in $before) {
132
+ if (-not $now.ContainsKey($child.Hwnd)) {
133
+ $changes += [ordered]@{ hwnd = $child.Hwnd; className = $child.ClassName; removed = $true; text = $child.Text }
134
+ }
135
+ }
136
+ , $changes
137
+ }
138
+
139
+ try {
140
+ $req = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($Request)) | ConvertFrom-Json
141
+ $window = [long]$req.window
142
+ $foregroundBefore = [CueloWin]::GetForegroundWindow().ToInt64()
143
+ if (-not [CueloWin]::Exists($window)) {
144
+ Write-Result ([ordered]@{ ok = $false; error = 'window-not-found'; window = $window }); exit 0
145
+ }
146
+ $before = [CueloWin]::Children($window)
147
+ if ($req.action -eq 'list') {
148
+ $foregroundAfter = [CueloWin]::GetForegroundWindow().ToInt64()
149
+ Write-Result ([ordered]@{ ok = $true; action = 'list'; window = $window; foregroundUnchanged = ($foregroundBefore -eq $foregroundAfter); controls = $before }); exit 0
150
+ }
151
+
152
+ $found = @($before | Where-Object {
153
+ ($null -eq $req.text -or $_.Text -ceq [string]$req.text) -and
154
+ ($null -eq $req.className -or $_.ClassName.IndexOf([string]$req.className, [StringComparison]::OrdinalIgnoreCase) -ge 0)
155
+ })
156
+ if ($found.Count -eq 0) {
157
+ Write-Result ([ordered]@{ ok = $false; error = 'control-not-found'; window = $window; controls = $before }); exit 0
158
+ }
159
+ if ($null -ne $req.index) {
160
+ $index = [int]$req.index
161
+ if ($index -lt 0 -or $index -ge $found.Count) {
162
+ Write-Result ([ordered]@{ ok = $false; error = 'index-out-of-range'; window = $window; matches = $found }); exit 0
163
+ }
164
+ $target = $found[$index]
165
+ } elseif ($found.Count -gt 1) {
166
+ Write-Result ([ordered]@{ ok = $false; error = 'ambiguous'; window = $window; matches = $found }); exit 0
167
+ } else {
168
+ $target = $found[0]
169
+ }
170
+
171
+ # 바꾸는 동작은 비활성 컨트롤에 보내지 않는다(사용자가 막아 둔 입력칸·버튼이다).
172
+ if ($req.action -ne 'read' -and -not $target.Enabled) {
173
+ Write-Result ([ordered]@{ ok = $false; error = 'control-disabled'; window = $window; target = $target }); exit 0
174
+ }
175
+ $code = 0
176
+ $started = [Diagnostics.Stopwatch]::StartNew()
177
+ switch ($req.action) {
178
+ 'setText' { $code = [CueloWin]::SetText($target.Hwnd, [string]$req.value) }
179
+ 'click' { $code = [CueloWin]::Click($target.Hwnd, [uint32]$req.timeoutMs) }
180
+ 'read' { }
181
+ default { throw "unknown action $($req.action)" }
182
+ }
183
+ $started.Stop()
184
+ $after = if ([CueloWin]::Exists($window)) { [CueloWin]::Children($window) } else { @() }
185
+ $foregroundAfter = [CueloWin]::GetForegroundWindow().ToInt64()
186
+ Write-Result ([ordered]@{
187
+ ok = ($code -eq 0)
188
+ action = $req.action
189
+ window = $window
190
+ windowClosed = -not [CueloWin]::Exists($window)
191
+ target = $target
192
+ targetAfter = ($after | Where-Object { $_.Hwnd -eq $target.Hwnd } | Select-Object -First 1)
193
+ win32Error = $code
194
+ elapsedMs = $started.ElapsedMilliseconds
195
+ foregroundUnchanged = ($foregroundBefore -eq $foregroundAfter)
196
+ changes = (Get-Changes $before $after)
197
+ })
198
+ } catch {
199
+ Write-Result ([ordered]@{ ok = $false; error = 'script-error'; message = $_.Exception.Message })
200
+ }
@@ -0,0 +1,255 @@
1
+ import type { CustomToolFactory } from "@oh-my-pi/pi-coding-agent";
2
+ import { posix } from "node:path";
3
+
4
+ // 2026-10-21 사무실 실측(doc/history/2026/10/21-computer-background): WinForms에서 computer의 UIA press·setValue는
5
+ // 효과는 있지만 사용자 전면 창을 가져가고, 게시된 win.click은 오류 없이 무시됐다. 같은 컨트롤 HWND에
6
+ // SendMessage WM_SETTEXT·BM_CLICK을 보내면 전면 창을 지키며 25~45ms에 동작했다. 이 도구가 그 경로다.
7
+
8
+ export type ControlAction = "list" | "read" | "setText" | "click";
9
+
10
+ export interface WindowsControlParams {
11
+ window: string | number;
12
+ action: ControlAction;
13
+ text?: string;
14
+ className?: string;
15
+ index?: number;
16
+ value?: string;
17
+ timeoutMs?: number;
18
+ }
19
+
20
+ export interface ExecResult { code: number; stdout: string; stderr?: string }
21
+ export type Exec = (command: string, args: string[], options: { cwd: string; timeout?: number }) => Promise<ExecResult>;
22
+
23
+ export interface ControlDeps {
24
+ exec: Exec;
25
+ cwd: string;
26
+ /** WSL이면 스크립트 경로를 wslpath -w로 바꾼다. */
27
+ wsl: boolean;
28
+ scriptPath: string;
29
+ }
30
+
31
+ export interface ChildControl {
32
+ Hwnd: number;
33
+ ClassName: string;
34
+ Text: string | null;
35
+ ControlId: number;
36
+ Visible: boolean;
37
+ Enabled: boolean;
38
+ /** -1: 상태를 읽을 수 없음(일반 버튼, WinForms처럼 직접 그리는 체크박스), 0: 해제, 1: 체크, 2: 불확정 */
39
+ Check: number;
40
+ }
41
+
42
+ export interface ControlChange {
43
+ hwnd: number;
44
+ className: string;
45
+ added?: boolean;
46
+ removed?: boolean;
47
+ text?: string | null;
48
+ textBefore?: string | null;
49
+ textAfter?: string | null;
50
+ checkBefore?: number;
51
+ checkAfter?: number;
52
+ enabledAfter?: boolean;
53
+ visibleAfter?: boolean;
54
+ }
55
+
56
+ export interface ControlResult {
57
+ ok: boolean;
58
+ error?: string;
59
+ message?: string;
60
+ action?: ControlAction;
61
+ window?: number;
62
+ windowClosed?: boolean;
63
+ target?: ChildControl;
64
+ targetAfter?: ChildControl | null;
65
+ win32Error?: number;
66
+ elapsedMs?: number;
67
+ foregroundUnchanged?: boolean;
68
+ changes?: ControlChange[] | ControlChange | null;
69
+ controls?: ChildControl[] | ChildControl | null;
70
+ matches?: ChildControl[] | ChildControl | null;
71
+ shell?: string;
72
+ }
73
+
74
+ const DEFAULT_CLICK_TIMEOUT_MS = 2000;
75
+ const MAX_LISTED = 40;
76
+ const WIN32_TIMEOUT = 1460;
77
+ const WIN32_ACCESS_DENIED = 5;
78
+
79
+ /** 요청을 검증해 스크립트로 넘길 JSON으로 만든다. 잘못된 요청은 Windows를 부르기 전에 거절한다. */
80
+ export function buildRequest(params: WindowsControlParams): Record<string, unknown> {
81
+ const window = Number(params.window);
82
+ if (!Number.isSafeInteger(window) || window <= 0) {
83
+ throw new Error(`windows_control: window는 computer.window()의 id(양의 정수 HWND)여야 한다: ${String(params.window)}`);
84
+ }
85
+ const needsTarget = params.action !== "list";
86
+ if (needsTarget && params.text === undefined && params.className === undefined) {
87
+ throw new Error("windows_control: list 말고는 text 또는 className으로 대상 컨트롤을 지정해야 한다");
88
+ }
89
+ if (params.action === "setText" && params.value === undefined) {
90
+ throw new Error("windows_control: setText에는 value가 필요하다");
91
+ }
92
+ if (params.index !== undefined && (!Number.isInteger(params.index) || params.index < 0)) {
93
+ throw new Error(`windows_control: index는 0 이상의 정수여야 한다: ${params.index}`);
94
+ }
95
+ const timeoutMs = Math.max(100, Math.min(30_000, Math.trunc(params.timeoutMs ?? DEFAULT_CLICK_TIMEOUT_MS)));
96
+ return {
97
+ window,
98
+ action: params.action,
99
+ ...(params.text !== undefined ? { text: params.text } : {}),
100
+ ...(params.className !== undefined ? { className: params.className } : {}),
101
+ ...(params.index !== undefined ? { index: params.index } : {}),
102
+ ...(params.value !== undefined ? { value: params.value } : {}),
103
+ timeoutMs,
104
+ };
105
+ }
106
+
107
+ export async function runWindowsControl(params: WindowsControlParams, deps: ControlDeps): Promise<ControlResult> {
108
+ const request = buildRequest(params);
109
+ let script = deps.scriptPath;
110
+ if (deps.wsl) {
111
+ const converted = await deps.exec("wslpath", ["-w", deps.scriptPath], { cwd: deps.cwd });
112
+ script = converted.stdout.trim();
113
+ if (converted.code !== 0 || !script) throw new Error(`windows_control: wslpath 변환 실패: ${converted.stderr ?? ""}`.trim());
114
+ }
115
+ const encoded = Buffer.from(JSON.stringify(request), "utf8").toString("base64");
116
+ const args = ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script, "-Request", encoded];
117
+ // 사용자 결정(10-09): Windows PowerShell 7(pwsh)을 먼저 쓰고, 없을 때만 5.1로 간다.
118
+ // "없음"은 pi.exec가 실행 파일을 찾지 못해 code ENOENT로 throw한 경우뿐이다. pwsh가 실행돼서 실패한 경우
119
+ // (어떤 종료 코드든)는 같은 요청을 다른 셸로 다시 보내지 않는다. 버튼을 두 번 누를 수 있어서다.
120
+ let shell = "pwsh.exe";
121
+ let result: ExecResult;
122
+ try {
123
+ result = await deps.exec(shell, args, { cwd: deps.cwd, timeout: 60_000 });
124
+ } catch (error) {
125
+ if ((error as NodeJS.ErrnoException | undefined)?.code !== "ENOENT") throw error;
126
+ shell = "powershell.exe";
127
+ result = await deps.exec(shell, args, { cwd: deps.cwd, timeout: 60_000 });
128
+ }
129
+ const line = result.stdout.split(/\r?\n/).map((text) => text.trim()).filter((text) => text.startsWith("{")).pop();
130
+ if (!line) {
131
+ const detail = [result.stderr, result.stdout].filter(Boolean).join("\n").trim().slice(0, 600);
132
+ throw new Error(`windows_control: ${shell} exit ${result.code}, 결과 JSON 없음${detail ? `: ${detail}` : ""}`);
133
+ }
134
+ return { ...(JSON.parse(line) as ControlResult), shell };
135
+ }
136
+
137
+ /** ConvertTo-Json은 원소가 하나인 배열을 객체로 내보낸다. */
138
+ function asList<T>(value: T[] | T | null | undefined): T[] {
139
+ if (value === null || value === undefined) return [];
140
+ return Array.isArray(value) ? value : [value];
141
+ }
142
+
143
+ function quote(text: string | null | undefined): string {
144
+ if (text === null || text === undefined) return "(읽기 실패)";
145
+ const flat = text.replace(/\r?\n/g, "⏎");
146
+ return JSON.stringify(flat.length > 80 ? `${flat.slice(0, 80)}…` : flat);
147
+ }
148
+
149
+ function describe(child: ChildControl): string {
150
+ const flags = [child.Visible ? "" : "숨김", child.Enabled ? "" : "비활성", child.Check >= 0 ? `check=${child.Check}` : ""].filter(Boolean);
151
+ return `${child.ClassName} ${quote(child.Text)} hwnd=${child.Hwnd}${child.ControlId ? ` id=${child.ControlId}` : ""}${flags.length ? ` (${flags.join(", ")})` : ""}`;
152
+ }
153
+
154
+ function describeChange(change: ControlChange): string {
155
+ if (change.added) return ` + ${change.className} hwnd=${change.hwnd} 새로 생김 ${quote(change.text)}`;
156
+ if (change.removed) return ` - ${change.className} hwnd=${change.hwnd} 사라짐 ${quote(change.text)}`;
157
+ const parts: string[] = [];
158
+ if (change.textBefore !== change.textAfter) parts.push(`${quote(change.textBefore)} → ${quote(change.textAfter)}`);
159
+ if (change.checkBefore !== change.checkAfter) parts.push(`check ${change.checkBefore} → ${change.checkAfter}`);
160
+ if (parts.length === 0) parts.push(`enabled=${change.enabledAfter} visible=${change.visibleAfter}`);
161
+ return ` ${change.className} hwnd=${change.hwnd}: ${parts.join(", ")}`;
162
+ }
163
+
164
+ function listBlock(title: string, children: ChildControl[]): string[] {
165
+ const shown = children.slice(0, MAX_LISTED).map((child) => ` ${describe(child)}`);
166
+ const more = children.length > MAX_LISTED ? [` … ${children.length - MAX_LISTED}개 더`] : [];
167
+ return [`${title} (${children.length}개):`, ...shown, ...more];
168
+ }
169
+
170
+ export function formatControlResult(result: ControlResult): string {
171
+ const lines: string[] = [];
172
+ if (!result.ok && result.error) {
173
+ switch (result.error) {
174
+ case "window-not-found":
175
+ return `windows_control: 창 hwnd=${result.window}가 없다. computer.window()로 id를 다시 확인한다.`;
176
+ case "control-not-found":
177
+ return [`windows_control: 조건에 맞는 컨트롤이 없다. 아무것도 보내지 않았다.`, ...listBlock("창의 컨트롤", asList(result.controls))].join("\n");
178
+ case "ambiguous":
179
+ return [`windows_control: 조건에 맞는 컨트롤이 여러 개다. index로 하나를 고른다. 아무것도 보내지 않았다.`, ...listBlock("후보(index 순서)", asList(result.matches))].join("\n");
180
+ case "index-out-of-range":
181
+ return [`windows_control: index가 후보 수를 넘는다. 아무것도 보내지 않았다.`, ...listBlock("후보(index 순서)", asList(result.matches))].join("\n");
182
+ case "control-disabled":
183
+ return `windows_control: 대상이 비활성이라 아무것도 보내지 않았다: ${result.target ? describe(result.target) : ""}`;
184
+ case "script-error":
185
+ return `windows_control: Windows 스크립트 오류: ${result.message ?? ""}`;
186
+ }
187
+ }
188
+ const foreground = result.foregroundUnchanged
189
+ ? "전면 창 유지."
190
+ : "주의: 호출 사이 전면 창이 바뀌었다(이 호출 때문인지 사용자 조작인지 이 결과로는 구분할 수 없다).";
191
+ if (result.action === "list") {
192
+ lines.push(...listBlock(`창 hwnd=${result.window}의 자식 컨트롤`, asList(result.controls)), foreground);
193
+ return lines.join("\n");
194
+ }
195
+ const target = result.target ? describe(result.target) : "(대상 미상)";
196
+ const verb = result.action === "click" ? "BM_CLICK" : result.action === "setText" ? "WM_SETTEXT" : "읽기";
197
+ if (result.ok) {
198
+ lines.push(`${verb} ${result.action === "read" ? "완료" : "전달"}: ${target}${result.elapsedMs !== undefined && result.action !== "read" ? ` (${result.elapsedMs}ms)` : ""}`);
199
+ } else if (result.win32Error === WIN32_TIMEOUT) {
200
+ lines.push(`${verb} 전달됐지만 처리기가 ${result.action === "click" ? "timeoutMs" : "2초"} 안에 끝나지 않았다(모달 창이 열렸을 수 있다): ${target}`);
201
+ } else if (result.win32Error === WIN32_ACCESS_DENIED) {
202
+ lines.push(`${verb} 거부됨(Win32 5): 대상 프로그램이 관리자 권한으로 실행 중이다. 아무 효과도 없다고 본다: ${target}`);
203
+ } else {
204
+ lines.push(`${verb} 실패(Win32 ${result.win32Error}): ${target}`);
205
+ }
206
+ if (result.action !== "read" && !result.windowClosed) {
207
+ lines.push(result.targetAfter ? `대상 지금: ${describe(result.targetAfter)}` : "대상 컨트롤이 사라졌다.");
208
+ }
209
+ if (result.windowClosed) lines.push("창이 닫혔다.");
210
+ const changes = asList(result.changes);
211
+ if (result.action !== "read" && !result.windowClosed) {
212
+ if (changes.length > 0) lines.push("창 안 컨트롤 변화:", ...changes.map(describeChange));
213
+ else lines.push("창 안 컨트롤의 텍스트·체크·활성 변화 없음. 효과는 AX나 화면으로 확인한다(새 창이 열렸으면 computer.windows()).");
214
+ }
215
+ lines.push(foreground);
216
+ return lines.join("\n");
217
+ }
218
+
219
+ const factory: CustomToolFactory = (pi) => ({
220
+ name: "windows_control",
221
+ label: "Windows Control",
222
+ loadMode: "essential",
223
+ description:
224
+ "Drive classic Win32 controls (WinForms, VB6, MFC: buttons, check boxes, text boxes) in a Windows window WITHOUT taking the user's foreground or focus. "
225
+ + "Sends `WM_SETTEXT` / `BM_CLICK` with SendMessageTimeout to the control's own child window, then reads every child control again and reports what changed (text, check state, enabled) and whether the foreground window stayed the same. "
226
+ + "Use it instead of computer `el.press()`/`el.setValue()` (which bring the window to the front on Windows) or `win.click` (silently ignored by WinForms buttons). "
227
+ + "`window` is the id from `computer.window()` (the top-level HWND). `list` shows the child controls; other actions pick one control by exact `text` and/or `className` substring, with `index` when several match (ambiguous picks are refused, nothing sent). "
228
+ + "Not for WPF, WinUI, UWP, web/Chromium or custom-drawn controls (they have no child window per control). "
229
+ + "Same authorization as computer input: only windows the user asked you to operate; confirm before consequential buttons (save, delete, send, start a machine).",
230
+ parameters: pi.zod.object({
231
+ window: pi.zod.union([pi.zod.string(), pi.zod.number()]).describe("Top-level window id from computer.window() (HWND)"),
232
+ action: pi.zod.enum(["list", "read", "setText", "click"]).describe("list child controls, read one, set its text, or click it"),
233
+ text: pi.zod.string().optional().describe("Exact current text/caption of the control, e.g. a button label"),
234
+ className: pi.zod.string().optional().describe("Case-insensitive substring of the Win32 class, e.g. EDIT, BUTTON, STATIC"),
235
+ index: pi.zod.number().optional().describe("0-based pick among several matches, in list order"),
236
+ value: pi.zod.string().optional().describe("New text for setText"),
237
+ timeoutMs: pi.zod.number().optional().describe("How long click waits for the button handler (default 2000; a modal dialog keeps it running)"),
238
+ }),
239
+
240
+ async execute(_toolCallId, params, _onUpdate, _ctx, signal) {
241
+ const exec: Exec = (command, args, options) => pi.exec(command, args, { ...options, signal });
242
+ const result = await runWindowsControl(params as WindowsControlParams, {
243
+ exec,
244
+ cwd: pi.cwd,
245
+ wsl: process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME),
246
+ scriptPath: posix.join(import.meta.dir, "control.ps1"),
247
+ });
248
+ return {
249
+ content: [{ type: "text", text: formatControlResult(result) }],
250
+ details: result,
251
+ };
252
+ },
253
+ });
254
+
255
+ export default factory;
@@ -2295,6 +2295,15 @@ function createRunPageScope(page: Page, restoreInterception: () => Promise<void>
2295
2295
  legacyPatched: "- Per-call `takeover: true` after `BackgroundUnavailable` needs no confirmation; use it instead of `computer.control.acquire`. Acquired control requires live human confirmation; acquire only on user request and release it when finished.",
2296
2296
  patched: "- After `BackgroundUnavailable`, follow the setting the error names. Under `takeover`, a per-call `takeover: true` needs no confirmation; use it instead of `computer.control.acquire`. Under `refuse` nothing was sent, and `takeover: true`, `raise()` and `control.acquire` are refused too: drive web pages through `browser` (relay) and controls through AX, or hand that step to the user. Acquired control requires live human confirmation; acquire only on user request and release it when finished.\n- Check the outcome in the same eval cell as the action: `wait()` for exact AX/DOM text or properties first. Only when the wording varies, pass the relevant AX text (not a screenshot) to `judge(state, {outcome: {type: \"choice\", instructions, criteria: {done, not_done, unknown}}})` instead of spending a model turn; treat `unknown` or low confidence as not done. Never let `judge()` decide focus, window or process identity, permissions, or whether input may be sent; those come from the OS and tool results.",
2297
2297
  },
2298
+ {
2299
+ // 2026-10-21 사무실 실측(doc/history/2026/10/21-computer-background): WinForms에서 AX press·setValue는 refuse에서도
2300
+ // 창을 앞으로 가져왔고, 게시된 win.click은 오류 없이 무시됐다(status unverified). 한 동작이 0.1~0.4초인데
2301
+ // 체감 지연은 동작 사이 모델 턴(p50 6.5~8.5초)이라, 한 셀 묶음과 windows_control 도구를 안내한다.
2302
+ file: "src/prompts/system/computer-use.md",
2303
+ marker: "Classic Win32 windows (WinForms, VB6, MFC)",
2304
+ anchor: "- Use AX for exposed semantic controls; use window screenshots and pixels for canvas/custom-drawn surfaces. Group predictable actions with verification in `computer.run`.",
2305
+ patched: "- Use AX for exposed semantic controls; use window screenshots and pixels for canvas/custom-drawn surfaces. Group predictable actions with verification in `computer.run`: one cell per act-wait-check sequence, not one model turn per click; each action takes well under a second, the turns between them are what the user waits for.\n- Classic Win32 windows (WinForms, VB6, MFC) on Windows: AX `press()`/`setValue()` on a window that is not in front either bring it to the foreground (even under `refuse`) or, on newer cores, fail with `BackgroundUnavailable`; a posted `win.click` on their buttons is silently ignored (`unverified`, no error). When the `windows_control` tool is available, use it for their buttons, check boxes and text boxes: it keeps the user's foreground and works while the window is covered. Treat any `unverified` action as not done until AX or the tool's readback shows the change.",
2306
+ },
2298
2307
  {
2299
2308
  // 빚의 정체(어느 steer 메시지인가)를 표현하는 최소 타입.
2300
2309
  file: "src/session/irc-bridge.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cuelo",
3
- "version": "0.10.49",
3
+ "version": "0.10.50",
4
4
  "description": "CUELO — open-source AI coding agent workspace and harness for omp (oh-my-pi)",
5
5
  "homepage": "https://github.com/gim47656-ship-it/CUELO#readme",
6
6
  "repository": {