@dmsdc-ai/aterm-darwin-arm64 0.1.44 → 0.1.46
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
|
|
|
@@ -25,21 +38,176 @@ ensure_lessons_file() {
|
|
|
25
38
|
[ -f "$f" ] || echo '{"invariants":[],"failed":[]}' > "$f"
|
|
26
39
|
}
|
|
27
40
|
|
|
41
|
+
print_ecosystem_status() {
|
|
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'
|
|
51
|
+
brain: ✅ installed
|
|
52
|
+
telepty: ✅ installed
|
|
53
|
+
deliberation: ✅ installed
|
|
54
|
+
devkit: ✅ installed
|
|
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
|
|
189
|
+
}
|
|
190
|
+
|
|
28
191
|
aterm_ipc() {
|
|
29
192
|
python3 -c '
|
|
30
|
-
import socket, sys, json
|
|
193
|
+
import os, socket, sys, json
|
|
31
194
|
|
|
32
195
|
sock_path = sys.argv[1]
|
|
33
196
|
action = sys.argv[2]
|
|
34
197
|
args = sys.argv[3:]
|
|
198
|
+
lang = os.environ.get("ATERM_UI_LANG", "en")
|
|
35
199
|
|
|
36
200
|
payloads = {
|
|
37
|
-
"list":
|
|
38
|
-
"inject":
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
"status":
|
|
42
|
-
"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"},
|
|
43
211
|
}
|
|
44
212
|
|
|
45
213
|
payload = json.dumps(payloads[action]()) + "\n"
|
|
@@ -69,7 +237,7 @@ try:
|
|
|
69
237
|
else:
|
|
70
238
|
sessions = resp.get("data", [])
|
|
71
239
|
if not sessions:
|
|
72
|
-
print("(no workspaces)")
|
|
240
|
+
print("(워크스페이스 없음)" if lang == "ko" else "(no workspaces)")
|
|
73
241
|
else:
|
|
74
242
|
import os.path
|
|
75
243
|
rows = []
|
|
@@ -79,7 +247,7 @@ try:
|
|
|
79
247
|
cwd = s.get("cwd", "")
|
|
80
248
|
terminal = s.get("terminal", "aterm")
|
|
81
249
|
rows.append((name, cli, cwd, terminal))
|
|
82
|
-
cols = ["NAME", "CLI", "CWD", "TERMINAL"]
|
|
250
|
+
cols = ["이름", "CLI", "경로", "터미널"] if lang == "ko" else ["NAME", "CLI", "CWD", "TERMINAL"]
|
|
83
251
|
widths = [max(len(c), *(len(r[i]) for r in rows)) for i, c in enumerate(cols)]
|
|
84
252
|
fmt = " ".join(f"{{:<{w}}}" for w in widths)
|
|
85
253
|
print(fmt.format(*cols))
|
|
@@ -150,6 +318,7 @@ print(json.dumps({"sessions": sessions}))
|
|
|
150
318
|
import json, sys, os, subprocess, os.path
|
|
151
319
|
|
|
152
320
|
sessions = []
|
|
321
|
+
lang = os.environ.get("ATERM_UI_LANG", "en")
|
|
153
322
|
|
|
154
323
|
# 1. aterm internal sessions
|
|
155
324
|
sock = os.environ.get("ATERM_IPC_SOCKET", "")
|
|
@@ -190,15 +359,21 @@ try:
|
|
|
190
359
|
except (FileNotFoundError, subprocess.TimeoutExpired, json.JSONDecodeError): pass
|
|
191
360
|
|
|
192
361
|
if not sessions:
|
|
193
|
-
print("(no reachable sessions)")
|
|
362
|
+
print("(도달 가능한 세션 없음)" if lang == "ko" else "(no reachable sessions)")
|
|
194
363
|
else:
|
|
195
|
-
cols = ["NAME", "CLI", "CWD", "TERMINAL"]
|
|
364
|
+
cols = ["이름", "CLI", "경로", "터미널"] if lang == "ko" else ["NAME", "CLI", "CWD", "TERMINAL"]
|
|
196
365
|
widths = [max(len(c), *(len(r[i]) for r in sessions)) for i, c in enumerate(cols)]
|
|
197
366
|
fmt = " ".join(f"{{:<{w}}}" for w in widths)
|
|
198
367
|
print(fmt.format(*cols))
|
|
199
368
|
print(" ".join("\u2500" * w for w in widths))
|
|
200
369
|
for r in sessions:
|
|
201
370
|
print(fmt.format(*r))
|
|
371
|
+
if all(r[3] == "aterm" for r in sessions):
|
|
372
|
+
print()
|
|
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.")
|
|
202
377
|
'
|
|
203
378
|
fi ;;
|
|
204
379
|
inject)
|
|
@@ -206,6 +381,10 @@ else:
|
|
|
206
381
|
[[ $# -lt 3 ]] && { echo '{"error":"usage: aterm inject <workspace> <text>"}' >&2; exit 1; }
|
|
207
382
|
aterm_ipc inject "${@:2}" ;;
|
|
208
383
|
status)
|
|
384
|
+
if [[ $# -eq 1 ]]; then
|
|
385
|
+
print_ecosystem_status
|
|
386
|
+
exit 0
|
|
387
|
+
fi
|
|
209
388
|
if [[ -z "$ATERM_IPC_SOCKET" ]]; then exec telepty status "${@:2}"; fi
|
|
210
389
|
[[ $# -lt 2 ]] && { echo '{"error":"usage: aterm status <workspace>"}' >&2; exit 1; }
|
|
211
390
|
aterm_ipc status "$2" ;;
|
|
@@ -213,6 +392,32 @@ else:
|
|
|
213
392
|
[[ -z "$ATERM_IPC_SOCKET" ]] && { echo '{"error":"focus requires aterm (ATERM_IPC_SOCKET not set)"}' >&2; exit 1; }
|
|
214
393
|
[[ $# -lt 2 ]] && { echo '{"error":"usage: aterm focus <workspace>"}' >&2; exit 1; }
|
|
215
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 ;;
|
|
216
421
|
tasks)
|
|
217
422
|
ensure_tasks_file
|
|
218
423
|
case "${2:-list}" in
|
|
@@ -295,56 +500,254 @@ print(json.dumps({"status":"ok","type": ltype}))
|
|
|
295
500
|
' "$(lessons_file)" "${@:3}" ;;
|
|
296
501
|
*) echo '{"error":"usage: aterm lessons [list|add]"}' >&2; exit 1 ;;
|
|
297
502
|
esac ;;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
|
301
512
|
|
|
302
|
-
|
|
513
|
+
# ── IPC helper ──────────────────────────────────────────────
|
|
303
514
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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()
|
|
317
534
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|-----------------|---------|
|
|
323
|
-
| 'list sessions' / '세션 목록 보여줘' | `aterm list` |
|
|
324
|
-
| 'send build to ghostty' / 'ghostty에 빌드 실행해줘' | `aterm inject ghostty 'make build'` |
|
|
325
|
-
| 'check cmux status' / 'cmux 세션 상태 확인해줘' | `aterm status cmux` |
|
|
326
|
-
| 'show tasks' / '태스크 목록 보여줘' | `aterm tasks` |
|
|
327
|
-
| 'add task: implement API' / '태스크 추가해줘: API 구현' | `aterm tasks add 'API 구현'` |
|
|
328
|
-
| 'task 42 done' / '태스크 42 완료' | `aterm tasks done 42` |
|
|
329
|
-
| 'show lessons' / '레슨 보여줘' | `aterm lessons` |
|
|
330
|
-
| 'add lesson: this pattern failed' / '레슨 추가' | `aterm lessons add 'this pattern failed'` |
|
|
331
|
-
|
|
332
|
-
### External sessions (other terminal/machine, $ATERM_IPC_SOCKET unset)
|
|
333
|
-
| Natural language | Command |
|
|
334
|
-
|-----------------|---------|
|
|
335
|
-
| 'list external sessions' / '외부 세션 목록' | `telepty list` |
|
|
336
|
-
| 'message deliberation session' | `telepty inject aigentry-deliberation-claude 'message'` |
|
|
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
|
+
})
|
|
337
539
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
+
})
|
|
342
545
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
546
|
+
def ipc_status(sock_path, workspace):
|
|
547
|
+
return ipc_send(sock_path, {
|
|
548
|
+
"action": "WorkspaceStatus", "workspace": workspace
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
def ipc_kill(sock_path, workspace):
|
|
552
|
+
return ipc_send(sock_path, {
|
|
553
|
+
"action": "CloseWorkspace", "workspace": workspace
|
|
554
|
+
})
|
|
555
|
+
|
|
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
|
|
348
751
|
;;
|
|
349
752
|
*) echo "{\"error\":\"unknown command: $1\"}" >&2; exit 1 ;;
|
|
350
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>
|