@dmsdc-ai/aterm-darwin-arm64 0.1.45 → 0.1.47
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.
|
Binary file
|
|
Binary file
|
|
@@ -5,6 +5,19 @@ set -euo pipefail
|
|
|
5
5
|
|
|
6
6
|
ATERM_IPC_SOCKET="${ATERM_IPC_SOCKET:-}"
|
|
7
7
|
|
|
8
|
+
detect_ui_lang() {
|
|
9
|
+
local raw="${ATERM_UI_LANG:-${LC_ALL:-${LC_MESSAGES:-${LANG:-}}}}"
|
|
10
|
+
raw="${raw,,}"
|
|
11
|
+
if [[ "$raw" == ko* || "$raw" == *"ko_"* || "$raw" == *"ko-"* ]]; then
|
|
12
|
+
echo "ko"
|
|
13
|
+
else
|
|
14
|
+
echo "en"
|
|
15
|
+
fi
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
ATERM_UI_LANG="$(detect_ui_lang)"
|
|
19
|
+
export ATERM_UI_LANG
|
|
20
|
+
|
|
8
21
|
# State directory for task board and lessons
|
|
9
22
|
STATE_DIR="${PWD}/state"
|
|
10
23
|
|
|
@@ -26,29 +39,175 @@ ensure_lessons_file() {
|
|
|
26
39
|
}
|
|
27
40
|
|
|
28
41
|
print_ecosystem_status() {
|
|
29
|
-
|
|
42
|
+
if [[ "$ATERM_UI_LANG" == "ko" ]]; then
|
|
43
|
+
cat <<'STATUS'
|
|
44
|
+
brain: ✅ 설치됨
|
|
45
|
+
telepty: ✅ 설치됨
|
|
46
|
+
deliberation: ✅ 설치됨
|
|
47
|
+
devkit: ✅ 설치됨
|
|
48
|
+
STATUS
|
|
49
|
+
else
|
|
50
|
+
cat <<'STATUS'
|
|
30
51
|
brain: ✅ installed
|
|
31
52
|
telepty: ✅ installed
|
|
32
53
|
deliberation: ✅ installed
|
|
33
54
|
devkit: ✅ installed
|
|
34
55
|
STATUS
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
show_help() {
|
|
60
|
+
if [[ "$ATERM_UI_LANG" == "ko" ]]; then
|
|
61
|
+
cat <<'HELP'
|
|
62
|
+
# aterm — AI 네이티브 터미널 세션 통신
|
|
63
|
+
|
|
64
|
+
## 명령어
|
|
65
|
+
|
|
66
|
+
| 명령어 | 설명 |
|
|
67
|
+
|--------|------|
|
|
68
|
+
| `aterm list` | 워크스페이스 목록 표시 (`이름`, `CLI`, `경로`, `터미널`) |
|
|
69
|
+
| `aterm inject <workspace> <text>` | 워크스페이스에 텍스트 전송 |
|
|
70
|
+
| `aterm status` | 번들된 에코시스템 상태 표시 |
|
|
71
|
+
| `aterm status <workspace>` | 워크스페이스 상태 확인 |
|
|
72
|
+
| `aterm focus <workspace>` | 워크스페이스 탭으로 포커스 이동 |
|
|
73
|
+
| `aterm create <name> --cli <cli> --cwd <path>` | 새 워크스페이스 생성 |
|
|
74
|
+
| `aterm restart <workspace>` | 죽은 세션 재시작 |
|
|
75
|
+
| `aterm kill <workspace>` | 워크스페이스 종료 |
|
|
76
|
+
| `aterm restart-all` | 죽은 세션 전체 재시작 |
|
|
77
|
+
| `aterm tasks` | 대기 중인 태스크 목록 표시 |
|
|
78
|
+
| `aterm tasks add '<description>'` | 새 태스크 추가 |
|
|
79
|
+
| `aterm tasks done <id>` | 태스크 완료 처리 |
|
|
80
|
+
| `aterm lessons` | 기록된 교훈 표시 |
|
|
81
|
+
| `aterm lessons add '<lesson>'` | 교훈 추가 (`invariant`) |
|
|
82
|
+
| `aterm lessons add '<lesson>' --type failed` | 실패 패턴 추가 |
|
|
83
|
+
| `aterm dispatch <task-id>` | 태스크 분해 → 서브세션 생성 → 실행 → 수집 → 정리 |
|
|
84
|
+
| `aterm dispatch --plan '<desc>'` | 자유 텍스트 태스크 → 동일한 dispatch 흐름 |
|
|
85
|
+
| `aterm help` | 이 도움말 표시 |
|
|
86
|
+
|
|
87
|
+
## 에코시스템
|
|
88
|
+
|
|
89
|
+
✅ brain (AI 프로필 관리) — 이미 설치됨
|
|
90
|
+
✅ telepty (터미널 간 통신) — 이미 설치됨
|
|
91
|
+
✅ deliberation (멀티 AI 토론) — 이미 설치됨
|
|
92
|
+
✅ devkit (워크스페이스 부트스트랩) — 이미 설치됨
|
|
93
|
+
|
|
94
|
+
## 자연어 → 명령어 매핑
|
|
95
|
+
|
|
96
|
+
### 내부 세션 (같은 aterm, `$ATERM_IPC_SOCKET` 설정됨)
|
|
97
|
+
| English request | 한국어 요청 | 명령어 |
|
|
98
|
+
|----------------|------------|--------|
|
|
99
|
+
| `list sessions` | `세션 목록 보여줘` | `aterm list` |
|
|
100
|
+
| `send build to ghostty` | `ghostty에 빌드 실행해줘` | `aterm inject ghostty 'make build'` |
|
|
101
|
+
| `check cmux status` | `cmux 세션 상태 확인해줘` | `aterm status cmux` |
|
|
102
|
+
| `show tasks` | `태스크 목록 보여줘` | `aterm tasks` |
|
|
103
|
+
| `add task: implement API` | `태스크 추가해줘: API 구현` | `aterm tasks add 'API 구현'` |
|
|
104
|
+
| `task 42 done` | `태스크 42 완료` | `aterm tasks done 42` |
|
|
105
|
+
| `show lessons` | `레슨 보여줘` | `aterm lessons` |
|
|
106
|
+
| `add lesson: this pattern failed` | `레슨 추가` | `aterm lessons add 'this pattern failed'` |
|
|
107
|
+
|
|
108
|
+
### 외부 세션 (다른 터미널/머신, `$ATERM_IPC_SOCKET` 미설정)
|
|
109
|
+
| English request | 한국어 요청 | 명령어 |
|
|
110
|
+
|----------------|------------|--------|
|
|
111
|
+
| `list external sessions` | `외부 세션 목록` | `telepty list` |
|
|
112
|
+
| `message deliberation session` | `deliberation 세션에 메시지 보내줘` | `telepty inject aigentry-deliberation-claude 'message'` |
|
|
113
|
+
|
|
114
|
+
## 세션 탐지 우선순위
|
|
115
|
+
1. `aterm list` — 내부 세션 (프로젝트 이름 + `terminal=aterm`)
|
|
116
|
+
2. `telepty list` — 외부 세션 (프로젝트 이름 + 터미널 타입)
|
|
117
|
+
3. `ps aux | grep` 를 주 탐지 수단으로 사용하지 말 것 (프로젝트 이름과 터미널 정보가 없음)
|
|
118
|
+
|
|
119
|
+
## 탐지 규칙
|
|
120
|
+
- `$ATERM_IPC_SOCKET` 이 설정되어 있으면 aterm 내부입니다. `aterm` 명령을 사용합니다.
|
|
121
|
+
- `$ATERM_IPC_SOCKET` 이 비어 있으면 aterm 외부입니다. `telepty` 명령을 사용합니다.
|
|
122
|
+
- `aterm` 은 `list`/`inject`/`status` 에서 자동으로 `telepty` 로 폴백합니다.
|
|
123
|
+
HELP
|
|
124
|
+
else
|
|
125
|
+
cat <<'HELP'
|
|
126
|
+
# aterm — AI-native terminal session communication
|
|
127
|
+
|
|
128
|
+
## Commands
|
|
129
|
+
|
|
130
|
+
| Command | Description |
|
|
131
|
+
|---------|-------------|
|
|
132
|
+
| `aterm list` | List workspaces (`NAME`, `CLI`, `CWD`, `TERMINAL`) |
|
|
133
|
+
| `aterm inject <workspace> <text>` | Send text to a workspace |
|
|
134
|
+
| `aterm status` | Show bundled ecosystem health |
|
|
135
|
+
| `aterm status <workspace>` | Check workspace status |
|
|
136
|
+
| `aterm focus <workspace>` | Focus a workspace tab |
|
|
137
|
+
| `aterm create <name> --cli <cli> --cwd <path>` | Create a new workspace |
|
|
138
|
+
| `aterm restart <workspace>` | Restart a dead session |
|
|
139
|
+
| `aterm kill <workspace>` | Close a workspace |
|
|
140
|
+
| `aterm restart-all` | Restart all dead sessions |
|
|
141
|
+
| `aterm tasks` | List pending tasks |
|
|
142
|
+
| `aterm tasks add '<description>'` | Add a new task |
|
|
143
|
+
| `aterm tasks done <id>` | Mark a task as completed |
|
|
144
|
+
| `aterm lessons` | List recorded lessons |
|
|
145
|
+
| `aterm lessons add '<lesson>'` | Add a lesson (`invariant`) |
|
|
146
|
+
| `aterm lessons add '<lesson>' --type failed` | Add a failed pattern |
|
|
147
|
+
| `aterm dispatch <task-id>` | Break down task -> create sub-sessions -> execute -> collect -> cleanup |
|
|
148
|
+
| `aterm dispatch --plan '<desc>'` | Free-text task -> same dispatch flow |
|
|
149
|
+
| `aterm help` | Show this help |
|
|
150
|
+
|
|
151
|
+
## Ecosystem
|
|
152
|
+
|
|
153
|
+
✅ brain (AI profile management) — already installed
|
|
154
|
+
✅ telepty (cross-terminal communication) — already installed
|
|
155
|
+
✅ deliberation (multi-AI discussions) — already installed
|
|
156
|
+
✅ devkit (workspace bootstrapping) — already installed
|
|
157
|
+
|
|
158
|
+
## Natural Language -> Command Mapping
|
|
159
|
+
|
|
160
|
+
### Internal sessions (same aterm, `$ATERM_IPC_SOCKET` set)
|
|
161
|
+
| English request | Korean request | Command |
|
|
162
|
+
|----------------|----------------|---------|
|
|
163
|
+
| `list sessions` | `세션 목록 보여줘` | `aterm list` |
|
|
164
|
+
| `send build to ghostty` | `ghostty에 빌드 실행해줘` | `aterm inject ghostty 'make build'` |
|
|
165
|
+
| `check cmux status` | `cmux 세션 상태 확인해줘` | `aterm status cmux` |
|
|
166
|
+
| `show tasks` | `태스크 목록 보여줘` | `aterm tasks` |
|
|
167
|
+
| `add task: implement API` | `태스크 추가해줘: API 구현` | `aterm tasks add 'API 구현'` |
|
|
168
|
+
| `task 42 done` | `태스크 42 완료` | `aterm tasks done 42` |
|
|
169
|
+
| `show lessons` | `레슨 보여줘` | `aterm lessons` |
|
|
170
|
+
| `add lesson: this pattern failed` | `레슨 추가` | `aterm lessons add 'this pattern failed'` |
|
|
171
|
+
|
|
172
|
+
### External sessions (other terminal or machine, `$ATERM_IPC_SOCKET` unset)
|
|
173
|
+
| English request | Korean request | Command |
|
|
174
|
+
|----------------|----------------|---------|
|
|
175
|
+
| `list external sessions` | `외부 세션 목록` | `telepty list` |
|
|
176
|
+
| `message deliberation session` | `deliberation 세션에 메시지 보내줘` | `telepty inject aigentry-deliberation-claude 'message'` |
|
|
177
|
+
|
|
178
|
+
## Session Detection Priority
|
|
179
|
+
1. `aterm list` — internal sessions (project names + `terminal=aterm`)
|
|
180
|
+
2. `telepty list` — external sessions (project names + terminal type)
|
|
181
|
+
3. Never use `ps aux | grep` as the primary detection method (no project names, no terminal info)
|
|
182
|
+
|
|
183
|
+
## Detection Rules
|
|
184
|
+
- `$ATERM_IPC_SOCKET` set -> inside aterm. Use `aterm` commands.
|
|
185
|
+
- `$ATERM_IPC_SOCKET` unset -> outside aterm. Use `telepty` commands.
|
|
186
|
+
- `aterm` automatically falls back to `telepty` for `list`, `inject`, and `status`.
|
|
187
|
+
HELP
|
|
188
|
+
fi
|
|
35
189
|
}
|
|
36
190
|
|
|
37
191
|
aterm_ipc() {
|
|
38
192
|
python3 -c '
|
|
39
|
-
import socket, sys, json
|
|
193
|
+
import os, socket, sys, json
|
|
40
194
|
|
|
41
195
|
sock_path = sys.argv[1]
|
|
42
196
|
action = sys.argv[2]
|
|
43
197
|
args = sys.argv[3:]
|
|
198
|
+
lang = os.environ.get("ATERM_UI_LANG", "en")
|
|
44
199
|
|
|
45
200
|
payloads = {
|
|
46
|
-
"list":
|
|
47
|
-
"inject":
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"status":
|
|
51
|
-
"focus":
|
|
201
|
+
"list": lambda: {"action": "ListWorkspaces"},
|
|
202
|
+
"inject": lambda: {"action": "Inject", "workspace": args[0],
|
|
203
|
+
"text": " ".join(args[1:]),
|
|
204
|
+
"from": __import__("os").environ.get("ATERM_WORKSPACE_NAME")},
|
|
205
|
+
"status": lambda: {"action": "WorkspaceStatus", "workspace": args[0]},
|
|
206
|
+
"focus": lambda: {"action": "FocusWorkspace", "workspace": args[0]},
|
|
207
|
+
"create": lambda: {"action": "CreateWorkspace", "name": args[0], "cli": args[1], "cwd": args[2]},
|
|
208
|
+
"kill": lambda: {"action": "CloseWorkspace", "workspace": args[0]},
|
|
209
|
+
"restart": lambda: {"action": "RestartWorkspace", "workspace": args[0]},
|
|
210
|
+
"restart-all": lambda: {"action": "RestartAllWorkspaces"},
|
|
52
211
|
}
|
|
53
212
|
|
|
54
213
|
payload = json.dumps(payloads[action]()) + "\n"
|
|
@@ -78,7 +237,7 @@ try:
|
|
|
78
237
|
else:
|
|
79
238
|
sessions = resp.get("data", [])
|
|
80
239
|
if not sessions:
|
|
81
|
-
print("(no workspaces)")
|
|
240
|
+
print("(워크스페이스 없음)" if lang == "ko" else "(no workspaces)")
|
|
82
241
|
else:
|
|
83
242
|
import os.path
|
|
84
243
|
rows = []
|
|
@@ -88,7 +247,7 @@ try:
|
|
|
88
247
|
cwd = s.get("cwd", "")
|
|
89
248
|
terminal = s.get("terminal", "aterm")
|
|
90
249
|
rows.append((name, cli, cwd, terminal))
|
|
91
|
-
cols = ["NAME", "CLI", "CWD", "TERMINAL"]
|
|
250
|
+
cols = ["이름", "CLI", "경로", "터미널"] if lang == "ko" else ["NAME", "CLI", "CWD", "TERMINAL"]
|
|
92
251
|
widths = [max(len(c), *(len(r[i]) for r in rows)) for i, c in enumerate(cols)]
|
|
93
252
|
fmt = " ".join(f"{{:<{w}}}" for w in widths)
|
|
94
253
|
print(fmt.format(*cols))
|
|
@@ -159,6 +318,7 @@ print(json.dumps({"sessions": sessions}))
|
|
|
159
318
|
import json, sys, os, subprocess, os.path
|
|
160
319
|
|
|
161
320
|
sessions = []
|
|
321
|
+
lang = os.environ.get("ATERM_UI_LANG", "en")
|
|
162
322
|
|
|
163
323
|
# 1. aterm internal sessions
|
|
164
324
|
sock = os.environ.get("ATERM_IPC_SOCKET", "")
|
|
@@ -199,9 +359,9 @@ try:
|
|
|
199
359
|
except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError): pass
|
|
200
360
|
|
|
201
361
|
if not sessions:
|
|
202
|
-
print("(no reachable sessions)")
|
|
362
|
+
print("(도달 가능한 세션 없음)" if lang == "ko" else "(no reachable sessions)")
|
|
203
363
|
else:
|
|
204
|
-
cols = ["NAME", "CLI", "CWD", "TERMINAL"]
|
|
364
|
+
cols = ["이름", "CLI", "경로", "터미널"] if lang == "ko" else ["NAME", "CLI", "CWD", "TERMINAL"]
|
|
205
365
|
widths = [max(len(c), *(len(r[i]) for r in sessions)) for i, c in enumerate(cols)]
|
|
206
366
|
fmt = " ".join(f"{{:<{w}}}" for w in widths)
|
|
207
367
|
print(fmt.format(*cols))
|
|
@@ -210,7 +370,10 @@ else:
|
|
|
210
370
|
print(fmt.format(*r))
|
|
211
371
|
if all(r[3] == "aterm" for r in sessions):
|
|
212
372
|
print()
|
|
213
|
-
|
|
373
|
+
if lang == "ko":
|
|
374
|
+
print("💡 현재 도달 가능한 세션은 모두 aterm 내부 세션입니다. 다른 터미널과 연결하려면 telepty를 사용하세요: 이미 포함되어 있습니다.")
|
|
375
|
+
else:
|
|
376
|
+
print("💡 All reachable sessions are internal to aterm. Use telepty for cross-terminal access: it is already included.")
|
|
214
377
|
'
|
|
215
378
|
fi ;;
|
|
216
379
|
inject)
|
|
@@ -229,6 +392,32 @@ else:
|
|
|
229
392
|
[[ -z "$ATERM_IPC_SOCKET" ]] && { echo '{"error":"focus requires aterm (ATERM_IPC_SOCKET not set)"}' >&2; exit 1; }
|
|
230
393
|
[[ $# -lt 2 ]] && { echo '{"error":"usage: aterm focus <workspace>"}' >&2; exit 1; }
|
|
231
394
|
aterm_ipc focus "$2" ;;
|
|
395
|
+
create)
|
|
396
|
+
[[ -z "$ATERM_IPC_SOCKET" ]] && { echo '{"error":"create requires aterm (ATERM_IPC_SOCKET not set)"}' >&2; exit 1; }
|
|
397
|
+
# Parse: aterm create <name> --cli <cli> --cwd <path>
|
|
398
|
+
shift
|
|
399
|
+
_name="${1:-}"; shift || true
|
|
400
|
+
_cli=""; _cwd=""
|
|
401
|
+
while [[ $# -gt 0 ]]; do
|
|
402
|
+
case "$1" in
|
|
403
|
+
--cli) _cli="$2"; shift 2 ;;
|
|
404
|
+
--cwd) _cwd="$2"; shift 2 ;;
|
|
405
|
+
*) shift ;;
|
|
406
|
+
esac
|
|
407
|
+
done
|
|
408
|
+
[[ -z "$_name" || -z "$_cli" || -z "$_cwd" ]] && { echo '{"error":"usage: aterm create <name> --cli <cli> --cwd <path>"}' >&2; exit 1; }
|
|
409
|
+
aterm_ipc create "$_name" "$_cli" "$_cwd" ;;
|
|
410
|
+
restart)
|
|
411
|
+
[[ -z "$ATERM_IPC_SOCKET" ]] && { echo '{"error":"restart requires aterm (ATERM_IPC_SOCKET not set)"}' >&2; exit 1; }
|
|
412
|
+
[[ $# -lt 2 ]] && { echo '{"error":"usage: aterm restart <workspace>"}' >&2; exit 1; }
|
|
413
|
+
aterm_ipc restart "$2" ;;
|
|
414
|
+
kill)
|
|
415
|
+
[[ -z "$ATERM_IPC_SOCKET" ]] && { echo '{"error":"kill requires aterm (ATERM_IPC_SOCKET not set)"}' >&2; exit 1; }
|
|
416
|
+
[[ $# -lt 2 ]] && { echo '{"error":"usage: aterm kill <workspace>"}' >&2; exit 1; }
|
|
417
|
+
aterm_ipc kill "$2" ;;
|
|
418
|
+
restart-all)
|
|
419
|
+
[[ -z "$ATERM_IPC_SOCKET" ]] && { echo '{"error":"restart-all requires aterm (ATERM_IPC_SOCKET not set)"}' >&2; exit 1; }
|
|
420
|
+
aterm_ipc restart-all ;;
|
|
232
421
|
tasks)
|
|
233
422
|
ensure_tasks_file
|
|
234
423
|
case "${2:-list}" in
|
|
@@ -311,64 +500,254 @@ print(json.dumps({"status":"ok","type": ltype}))
|
|
|
311
500
|
' "$(lessons_file)" "${@:3}" ;;
|
|
312
501
|
*) echo '{"error":"usage: aterm lessons [list|add]"}' >&2; exit 1 ;;
|
|
313
502
|
esac ;;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
503
|
+
dispatch)
|
|
504
|
+
[[ -z "$ATERM_IPC_SOCKET" ]] && { echo '{"error":"dispatch requires aterm (ATERM_IPC_SOCKET not set)"}' >&2; exit 1; }
|
|
505
|
+
python3 -c '
|
|
506
|
+
import socket
|
|
507
|
+
import subprocess
|
|
508
|
+
import json
|
|
509
|
+
import sys
|
|
510
|
+
import time
|
|
511
|
+
import os
|
|
317
512
|
|
|
318
|
-
|
|
513
|
+
# ── IPC helper ──────────────────────────────────────────────
|
|
319
514
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
515
|
+
def ipc_send(sock_path, payload):
|
|
516
|
+
"""Send JSON to aterm IPC socket and return parsed response."""
|
|
517
|
+
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
518
|
+
try:
|
|
519
|
+
s.settimeout(10)
|
|
520
|
+
s.connect(sock_path)
|
|
521
|
+
s.sendall((json.dumps(payload) + "\n").encode())
|
|
522
|
+
s.shutdown(socket.SHUT_WR)
|
|
523
|
+
data = b""
|
|
524
|
+
while True:
|
|
525
|
+
chunk = s.recv(4096)
|
|
526
|
+
if not chunk:
|
|
527
|
+
break
|
|
528
|
+
data += chunk
|
|
529
|
+
if b"\n" in data:
|
|
530
|
+
break
|
|
531
|
+
return json.loads(data.decode().strip().split("\n")[0])
|
|
532
|
+
finally:
|
|
533
|
+
s.close()
|
|
334
534
|
|
|
335
|
-
|
|
535
|
+
def ipc_create(sock_path, name, cli, cwd):
|
|
536
|
+
return ipc_send(sock_path, {
|
|
537
|
+
"action": "CreateWorkspace", "name": name, "cli": cli, "cwd": cwd
|
|
538
|
+
})
|
|
336
539
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
540
|
+
def ipc_inject(sock_path, workspace, text):
|
|
541
|
+
return ipc_send(sock_path, {
|
|
542
|
+
"action": "Inject", "workspace": workspace, "text": text,
|
|
543
|
+
"from": os.environ.get("ATERM_WORKSPACE_NAME")
|
|
544
|
+
})
|
|
341
545
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|-----------------|---------|
|
|
347
|
-
| 'list sessions' / '세션 목록 보여줘' | `aterm list` |
|
|
348
|
-
| 'send build to ghostty' / 'ghostty에 빌드 실행해줘' | `aterm inject ghostty 'make build'` |
|
|
349
|
-
| 'check cmux status' / 'cmux 세션 상태 확인해줘' | `aterm status cmux` |
|
|
350
|
-
| 'show tasks' / '태스크 목록 보여줘' | `aterm tasks` |
|
|
351
|
-
| 'add task: implement API' / '태스크 추가해줘: API 구현' | `aterm tasks add 'API 구현'` |
|
|
352
|
-
| 'task 42 done' / '태스크 42 완료' | `aterm tasks done 42` |
|
|
353
|
-
| 'show lessons' / '레슨 보여줘' | `aterm lessons` |
|
|
354
|
-
| 'add lesson: this pattern failed' / '레슨 추가' | `aterm lessons add 'this pattern failed'` |
|
|
355
|
-
|
|
356
|
-
### External sessions (other terminal/machine, $ATERM_IPC_SOCKET unset)
|
|
357
|
-
| Natural language | Command |
|
|
358
|
-
|-----------------|---------|
|
|
359
|
-
| 'list external sessions' / '외부 세션 목록' | `telepty list` |
|
|
360
|
-
| 'message deliberation session' | `telepty inject aigentry-deliberation-claude 'message'` |
|
|
546
|
+
def ipc_status(sock_path, workspace):
|
|
547
|
+
return ipc_send(sock_path, {
|
|
548
|
+
"action": "WorkspaceStatus", "workspace": workspace
|
|
549
|
+
})
|
|
361
550
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
551
|
+
def ipc_kill(sock_path, workspace):
|
|
552
|
+
return ipc_send(sock_path, {
|
|
553
|
+
"action": "CloseWorkspace", "workspace": workspace
|
|
554
|
+
})
|
|
366
555
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
-
|
|
371
|
-
|
|
556
|
+
# ── Task breakdown ──────────────────────────────────────────
|
|
557
|
+
|
|
558
|
+
def breakdown_task(description):
|
|
559
|
+
"""Try aigentry-devkit breakdown; fall back to single subtask."""
|
|
560
|
+
try:
|
|
561
|
+
result = subprocess.run(
|
|
562
|
+
["aigentry-devkit", "breakdown", description],
|
|
563
|
+
capture_output=True, text=True, timeout=30
|
|
564
|
+
)
|
|
565
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
566
|
+
data = json.loads(result.stdout)
|
|
567
|
+
subtasks = data if isinstance(data, list) else data.get("subtasks", [])
|
|
568
|
+
if subtasks:
|
|
569
|
+
return subtasks
|
|
570
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError, Exception):
|
|
571
|
+
pass
|
|
572
|
+
return [{"description": description}]
|
|
573
|
+
|
|
574
|
+
# ── CLI auto-selection ──────────────────────────────────────
|
|
575
|
+
|
|
576
|
+
def select_cli(description):
|
|
577
|
+
"""Pick CLI based on keywords in the subtask description."""
|
|
578
|
+
desc_lower = description.lower()
|
|
579
|
+
codex_keywords = ["implement", "code", "build", "write", "fix bug", "create file", "add function"]
|
|
580
|
+
claude_keywords = ["architect", "debug", "analyze", "design", "refactor", "review"]
|
|
581
|
+
gemini_keywords = ["research", "document", "search", "summarize", "find information"]
|
|
582
|
+
for kw in codex_keywords:
|
|
583
|
+
if kw in desc_lower:
|
|
584
|
+
return "codex"
|
|
585
|
+
for kw in claude_keywords:
|
|
586
|
+
if kw in desc_lower:
|
|
587
|
+
return "claude"
|
|
588
|
+
for kw in gemini_keywords:
|
|
589
|
+
if kw in desc_lower:
|
|
590
|
+
return "gemini"
|
|
591
|
+
return "claude"
|
|
592
|
+
|
|
593
|
+
# ── Poll for completion ─────────────────────────────────────
|
|
594
|
+
|
|
595
|
+
def poll_session(sock_path, name, timeout=300, interval=10):
|
|
596
|
+
"""Poll workspace status until idle or timeout."""
|
|
597
|
+
elapsed = 0
|
|
598
|
+
while elapsed < timeout:
|
|
599
|
+
time.sleep(interval)
|
|
600
|
+
elapsed += interval
|
|
601
|
+
try:
|
|
602
|
+
resp = ipc_status(sock_path, name)
|
|
603
|
+
status_data = resp.get("data", resp)
|
|
604
|
+
state = status_data.get("state", "")
|
|
605
|
+
if state == "idle":
|
|
606
|
+
return "complete"
|
|
607
|
+
except Exception:
|
|
608
|
+
pass
|
|
609
|
+
return "timeout"
|
|
610
|
+
|
|
611
|
+
# ── Main ────────────────────────────────────────────────────
|
|
612
|
+
|
|
613
|
+
def main():
|
|
614
|
+
if len(sys.argv) < 2:
|
|
615
|
+
print(json.dumps({"error": "usage: aterm dispatch <task-id> | --plan <description>"}), file=sys.stderr)
|
|
616
|
+
sys.exit(1)
|
|
617
|
+
|
|
618
|
+
sock_path = sys.argv[1]
|
|
619
|
+
tasks_file = sys.argv[2]
|
|
620
|
+
args = sys.argv[3:]
|
|
621
|
+
|
|
622
|
+
if not args:
|
|
623
|
+
print(json.dumps({"error": "usage: aterm dispatch <task-id> | --plan <description>"}), file=sys.stderr)
|
|
624
|
+
sys.exit(1)
|
|
625
|
+
|
|
626
|
+
# Parse: --plan or task-id
|
|
627
|
+
task_id = None
|
|
628
|
+
description = None
|
|
629
|
+
|
|
630
|
+
if args[0] == "--plan":
|
|
631
|
+
if len(args) < 2:
|
|
632
|
+
print(json.dumps({"error": "usage: aterm dispatch --plan <description>"}), file=sys.stderr)
|
|
633
|
+
sys.exit(1)
|
|
634
|
+
description = " ".join(args[1:])
|
|
635
|
+
task_id = "plan"
|
|
636
|
+
else:
|
|
637
|
+
task_id = args[0]
|
|
638
|
+
try:
|
|
639
|
+
tid = int(task_id)
|
|
640
|
+
except ValueError:
|
|
641
|
+
print(json.dumps({"error": f"invalid task-id: {task_id}"}), file=sys.stderr)
|
|
642
|
+
sys.exit(1)
|
|
643
|
+
# Read from task-queue.json
|
|
644
|
+
try:
|
|
645
|
+
with open(tasks_file) as f:
|
|
646
|
+
data = json.load(f)
|
|
647
|
+
found = None
|
|
648
|
+
for t in data.get("tasks", []):
|
|
649
|
+
if isinstance(t, dict) and t.get("id") == tid:
|
|
650
|
+
found = t
|
|
651
|
+
break
|
|
652
|
+
if not found:
|
|
653
|
+
print(json.dumps({"error": f"task {tid} not found in {tasks_file}"}), file=sys.stderr)
|
|
654
|
+
sys.exit(1)
|
|
655
|
+
description = found.get("description", "")
|
|
656
|
+
except (FileNotFoundError, json.JSONDecodeError) as e:
|
|
657
|
+
print(json.dumps({"error": f"cannot read tasks file: {e}"}), file=sys.stderr)
|
|
658
|
+
sys.exit(1)
|
|
659
|
+
|
|
660
|
+
# Breakdown
|
|
661
|
+
subtasks = breakdown_task(description)
|
|
662
|
+
|
|
663
|
+
# Subagent vs subsession judgment
|
|
664
|
+
if len(subtasks) == 1:
|
|
665
|
+
desc = subtasks[0].get("description", subtasks[0]) if isinstance(subtasks[0], dict) else str(subtasks[0])
|
|
666
|
+
# Check if description mentions a single file
|
|
667
|
+
single_file_hints = [".rs", ".ts", ".js", ".py", ".swift", ".go", ".java", ".c", ".cpp", ".h", ".md"]
|
|
668
|
+
if any(hint in desc for hint in single_file_hints):
|
|
669
|
+
print(json.dumps({
|
|
670
|
+
"recommendation": "subagent",
|
|
671
|
+
"reason": "single file, simple task",
|
|
672
|
+
"task_id": task_id,
|
|
673
|
+
"description": desc
|
|
674
|
+
}))
|
|
675
|
+
sys.exit(0)
|
|
676
|
+
|
|
677
|
+
# Create sub-sessions, inject, poll, collect, kill
|
|
678
|
+
cwd = os.getcwd()
|
|
679
|
+
sessions_created = []
|
|
680
|
+
reports = []
|
|
681
|
+
|
|
682
|
+
for i, st in enumerate(subtasks):
|
|
683
|
+
desc = st.get("description", st) if isinstance(st, dict) else str(st)
|
|
684
|
+
cli = select_cli(desc)
|
|
685
|
+
name = f"dispatch-{task_id}-sub{i}"
|
|
686
|
+
|
|
687
|
+
# Create session
|
|
688
|
+
try:
|
|
689
|
+
ipc_create(sock_path, name, cli, cwd)
|
|
690
|
+
sessions_created.append(name)
|
|
691
|
+
except Exception as e:
|
|
692
|
+
reports.append({"name": name, "status": "create_failed", "cli": cli, "error": str(e)})
|
|
693
|
+
continue
|
|
694
|
+
|
|
695
|
+
# Wait for session to start
|
|
696
|
+
time.sleep(2)
|
|
697
|
+
|
|
698
|
+
# Inject task description
|
|
699
|
+
try:
|
|
700
|
+
ipc_inject(sock_path, name, desc)
|
|
701
|
+
except Exception as e:
|
|
702
|
+
reports.append({"name": name, "status": "inject_failed", "cli": cli, "error": str(e)})
|
|
703
|
+
continue
|
|
704
|
+
|
|
705
|
+
# Poll all sessions for completion
|
|
706
|
+
for name in sessions_created:
|
|
707
|
+
# Skip sessions that already failed
|
|
708
|
+
if any(r["name"] == name for r in reports):
|
|
709
|
+
continue
|
|
710
|
+
cli = "unknown"
|
|
711
|
+
for i, st in enumerate(subtasks):
|
|
712
|
+
if f"dispatch-{task_id}-sub{i}" == name:
|
|
713
|
+
desc = st.get("description", st) if isinstance(st, dict) else str(st)
|
|
714
|
+
cli = select_cli(desc)
|
|
715
|
+
break
|
|
716
|
+
status = poll_session(sock_path, name)
|
|
717
|
+
reports.append({"name": name, "status": status, "cli": cli})
|
|
718
|
+
|
|
719
|
+
# Kill all sub-sessions
|
|
720
|
+
for name in sessions_created:
|
|
721
|
+
try:
|
|
722
|
+
ipc_kill(sock_path, name)
|
|
723
|
+
except Exception:
|
|
724
|
+
pass
|
|
725
|
+
|
|
726
|
+
# Determine overall status
|
|
727
|
+
statuses = [r["status"] for r in reports]
|
|
728
|
+
if all(s == "complete" for s in statuses):
|
|
729
|
+
overall = "all_complete"
|
|
730
|
+
elif all(s == "timeout" for s in statuses):
|
|
731
|
+
overall = "all_timeout"
|
|
732
|
+
elif any(s == "complete" for s in statuses):
|
|
733
|
+
overall = "partial_complete"
|
|
734
|
+
else:
|
|
735
|
+
overall = "failed"
|
|
736
|
+
|
|
737
|
+
result = {
|
|
738
|
+
"task_id": task_id if task_id == "plan" else int(task_id),
|
|
739
|
+
"subtasks": len(subtasks),
|
|
740
|
+
"sessions_created": sessions_created,
|
|
741
|
+
"status": overall,
|
|
742
|
+
"reports": reports
|
|
743
|
+
}
|
|
744
|
+
print(json.dumps(result, indent=2))
|
|
745
|
+
|
|
746
|
+
main()
|
|
747
|
+
' "$ATERM_IPC_SOCKET" "$(tasks_file)" "${@:2}"
|
|
748
|
+
;;
|
|
749
|
+
help|--help|-h)
|
|
750
|
+
show_help
|
|
372
751
|
;;
|
|
373
752
|
*) echo "{\"error\":\"unknown command: $1\"}" >&2; exit 1 ;;
|
|
374
753
|
esac
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<dict>
|
|
7
7
|
<key>Resources/bin/aterm</key>
|
|
8
8
|
<data>
|
|
9
|
-
|
|
9
|
+
B0IY8YjfGFvwfuMtAo6dKNuwNqY=
|
|
10
10
|
</data>
|
|
11
11
|
</dict>
|
|
12
12
|
<key>files2</key>
|
|
@@ -15,16 +15,16 @@
|
|
|
15
15
|
<dict>
|
|
16
16
|
<key>cdhash</key>
|
|
17
17
|
<data>
|
|
18
|
-
|
|
18
|
+
5x/w06jX/30THayLISHAxO7Ujv8=
|
|
19
19
|
</data>
|
|
20
20
|
<key>requirement</key>
|
|
21
|
-
<string>cdhash H"
|
|
21
|
+
<string>cdhash H"e71ff0d3a8d7ff7d131dac8b2121c0c4eed48eff"</string>
|
|
22
22
|
</dict>
|
|
23
23
|
<key>Resources/bin/aterm</key>
|
|
24
24
|
<dict>
|
|
25
25
|
<key>hash2</key>
|
|
26
26
|
<data>
|
|
27
|
-
|
|
27
|
+
2WJQSujew/jyCHX49GN82hlmuOg+XHTuYjlUxkHpK0Q=
|
|
28
28
|
</data>
|
|
29
29
|
</dict>
|
|
30
30
|
</dict>
|