Merge branch 'codex/owner/ejclaw'

This commit is contained in:
ejclaw
2026-04-08 10:10:42 +09:00
34 changed files with 393 additions and 10080 deletions

View File

@@ -1,15 +1,16 @@
---
name: setup
description: Run initial EJClaw setup for the dual-service Discord architecture.
description: Run initial EJClaw setup for the unified single-service Discord architecture.
---
# EJClaw Setup
설치는 `bash setup.sh`로 부트스트랩하고, 나머지는 `bun run setup -- --step <name>`으로 진행합니다. 현재 기준 채널은 디스코드만 지원합니다.
EJClaw는 두 개의 서비스로 구성됩니다:
- **ejclaw** — Claude Code 봇 (`@claude`)
- **ejclaw-codex** — Codex 봇 (`@codex`) — `.env.codex` 파일이 있으면 자동 설치
EJClaw는 현재 **단일 서비스**로 동작합니다:
- **ejclaw** — 통합 런타임 서비스
Claude가 기본 봇으로 동작하고, Codex는 별도 서비스가 아니라 필요 시 내부 러너/역할 라우팅으로 사용됩니다.
## 1. 부트스트랩
@@ -34,7 +35,7 @@ bun run setup -- --step environment
## 3. 필수 환경 변수
### Claude 서비스 (.env)
### 기본 환경 변수 (.env)
`.env`에 최소한 아래 값이 있어야 합니다.
@@ -51,18 +52,10 @@ CLAUDE_CODE_OAUTH_TOKENS=token1,token2 # 다중 계정 자동 로테이션
GROQ_API_KEY=... # Discord 음성 전사 (Groq Whisper)
```
### Codex 서비스 (.env.codex)
Codex 봇을 함께 운영하려면 `.env.codex`를 만듭니다. 이 파일이 있으면 `--step service`가 자동으로 `ejclaw-codex` 서비스도 설치합니다.
Codex 관련 선택 설정은 **별도 서비스 파일이 아니라 같은 `.env` 또는 서비스 환경 변수**에 넣습니다:
```bash
DISCORD_BOT_TOKEN=... # Codex 봇 토큰 (Claude와 별도)
```
Codex 서비스의 추가 설정은 systemd 유닛에서 `Environment=` 라인으로 지정하거나 `.env.codex`에 추가합니다:
```bash
# systemd 유닛 또는 .env.codex에 추가 가능
# 같은 .env 또는 서비스 Environment=에 추가 가능
CODEX_MODEL=gpt-5.4
CODEX_EFFORT=xhigh
OPENAI_API_KEY=...
@@ -91,15 +84,17 @@ bun run setup -- --step runners
- `runners/agent-runner` (Claude Code)
- `runners/codex-runner` (Codex)
서비스는 하나지만, 내부 역할 라우팅과 paired 흐름에서 두 러너를 모두 쓸 수 있어서 둘 다 빌드합니다.
실패하면 보통 `bun run build:runners` 출력과 각 러너의 `package.json` 의존성을 같이 보면 됩니다.
## 5. 디스코드 채널 등록
먼저 디스코드에서 개발자 모드를 켜고 채널 ID를 복사합니다. 등록 JID는 `dc:<channel_id>` 형식입니다.
듀얼 서비스에서는 **같은 채널을 두 번 등록**할 수 있습니다 — 각 agent type에 한 번씩. 등록은 `(jid, agent_type)` 복합 키로 저장됩니다.
채널 등록은 기본적으로 한 번 하면 됩니다.
Claude 봇 채널 등록:
예시:
```bash
bun run setup -- --step register -- \
@@ -111,18 +106,6 @@ bun run setup -- --step register -- \
--no-trigger-required
```
Codex 봇도 같은 채널을 쓴다면 별도로 등록합니다:
```bash
ASSISTANT_NAME=codex bun run setup -- --step register -- \
--jid dc:123456789012345678 \
--name "My Server #general" \
--folder discord_main \
--trigger @codex \
--is-main \
--no-trigger-required
```
보조 채널 예시:
```bash
@@ -140,24 +123,22 @@ bun run setup -- --step service
```
이 명령은:
- **ejclaw** 서비스를 항상 설치합니다
- `.env.codex`가 있으면 **ejclaw-codex** 서비스도 함께 설치합니다
- **ejclaw** 서비스를 설치하고 시작합니다
플랫폼별:
- Linux (systemd): `~/.config/systemd/user/ejclaw.service` + `ejclaw-codex.service`
- macOS: `~/Library/LaunchAgents/com.ejclaw.plist` + `com.ejclaw-codex.plist`
- WSL (no systemd): `start-ejclaw.sh` + `start-ejclaw-codex.sh`
- Linux (systemd): `~/.config/systemd/user/ejclaw.service`
- macOS: `~/Library/LaunchAgents/com.ejclaw.plist`
- WSL (no systemd): `start-ejclaw.sh`
수동으로 서비스 관리:
```bash
# Linux (systemd)
systemctl --user status ejclaw ejclaw-codex
systemctl --user restart ejclaw ejclaw-codex
systemctl --user status ejclaw
systemctl --user restart ejclaw
# 로그
journalctl --user -u ejclaw -f
journalctl --user -u ejclaw-codex -f
```
## 7. 최종 검증
@@ -169,7 +150,6 @@ bun run setup -- --step verify
성공 기준:
- **ejclaw** 서비스가 running
- **ejclaw-codex** 서비스가 running (`.env.codex`가 있을 때)
- Claude 인증이 configured
- `CHANNEL_AUTH``discord`
- 등록 그룹 수가 1 이상
@@ -178,6 +158,6 @@ bun run setup -- --step verify
- 빌드 문제: `bun run typecheck`, `bun test`, `bun run build:runners`
- Claude 서비스 문제: `logs/ejclaw.error.log` 또는 `journalctl --user -u ejclaw -f`
- Codex 서비스 문제: `logs/ejclaw-codex.error.log` 또는 `journalctl --user -u ejclaw-codex -f`
- Codex 실행 문제: 같은 `ejclaw` 서비스 로그에서 `CODEX_MODEL`, `CODEX_EFFORT`, `OPENAI_API_KEY` 설정을 같이 확인
- 디스코드 연결 문제: `.env``DISCORD_BOT_TOKEN`과 등록된 `dc:*` JID 확인
- 응답 문제: `tail -f logs/ejclaw.log`

2
.github/CODEOWNERS vendored
View File

@@ -3,7 +3,7 @@
/groups/ @gavrielc @gabi-simons
/launchd/ @gavrielc @gabi-simons
/package.json @gavrielc @gabi-simons
/package-lock.json @gavrielc @gabi-simons
/bun.lock @gavrielc @gabi-simons
# Skills - open to contributors
/.claude/skills/

3
.gitignore vendored
View File

@@ -48,7 +48,4 @@ runners/codex-runner-backups/
.idea/
.vscode/
# Skills system (local per-installation state)
.nanoclaw/
agents-sdk-docs

View File

@@ -1,3 +0,0 @@
{
"mcpServers": {}
}

View File

@@ -1,25 +0,0 @@
{
"allowedRoots": [
{
"path": "~/projects",
"allowReadWrite": true,
"description": "Development projects"
},
{
"path": "~/repos",
"allowReadWrite": true,
"description": "Git repositories"
},
{
"path": "~/Documents/work",
"allowReadWrite": false,
"description": "Work documents (read-only)"
}
],
"blockedPatterns": [
"password",
"secret",
"token"
],
"nonMainReadOnly": true
}

View File

@@ -1,13 +0,0 @@
# Codex Assistant
You are a participant in a Discord chat. Your name is "코덱스" (Codex).
This channel works with the nanoclaw project at /home/clone-ej/nanoclaw.
## Core Rules
- Respond directly to messages. Do NOT give "reply suggestions" or "here's how you could respond".
- When someone asks a question, answer it yourself. You are having a conversation, not helping someone draft replies.
- Speak in natural, friendly Korean.
- When coding, debugging, or file work is needed, do it directly.
- When you reach a conclusion or need human judgment, mention @눈쟁이 in your message.

View File

@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.ejclaw-codex</string>
<key>ProgramArguments</key>
<array>
<string>{{NODE_PATH}}</string>
<string>{{PROJECT_ROOT}}/dist/index.js</string>
</array>
<key>WorkingDirectory</key>
<string>{{PROJECT_ROOT}}</string>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>{{HOME}}/.npm-global/bin:{{HOME}}/.local/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>HOME</key>
<string>{{HOME}}</string>
<key>ASSISTANT_NAME</key>
<string>Andy-Codex</string>
<key>EJCLAW_STORE_DIR</key>
<string>{{PROJECT_ROOT}}/store-codex</string>
<key>EJCLAW_GROUPS_DIR</key>
<string>{{PROJECT_ROOT}}/groups-codex</string>
<key>EJCLAW_DATA_DIR</key>
<string>{{PROJECT_ROOT}}/data-codex</string>
</dict>
<key>StandardOutPath</key>
<string>{{PROJECT_ROOT}}/logs/ejclaw-codex.log</string>
<key>StandardErrorPath</key>
<string>{{PROJECT_ROOT}}/logs/ejclaw-codex.error.log</string>
</dict>
</plist>

3985
package-lock.json generated

File diff suppressed because it is too large Load Diff

2636
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
"version": "1.0.0",
"type": "module",
"description": "Agent runner subprocess for EJClaw",
"packageManager": "bun@1.3.11",
"main": "dist/index.js",
"scripts": {
"build": "tsc",

View File

@@ -1,994 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@anthropic-ai/claude-agent-sdk':
specifier: ^0.2.81
version: 0.2.81(zod@4.3.6)
'@modelcontextprotocol/sdk':
specifier: ^1.12.1
version: 1.27.1(zod@4.3.6)
cron-parser:
specifier: ^5.0.0
version: 5.5.0
zod:
specifier: ^4.0.0
version: 4.3.6
devDependencies:
'@types/node':
specifier: ^22.10.7
version: 22.19.15
typescript:
specifier: ^5.7.3
version: 5.9.3
packages:
'@anthropic-ai/claude-agent-sdk@0.2.81':
resolution: {integrity: sha512-CBeebgibBEN/DWOQGZN67vhuTG55RbI1hlsFSSoZ4uA/Io3lw04eHTE2ISCmdbqyJaefYTt6GKZei1nP0TQMNw==}
engines: {node: '>=18.0.0'}
peerDependencies:
zod: ^4.0.0
'@hono/node-server@1.19.11':
resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==}
engines: {node: '>=18.14.1'}
peerDependencies:
hono: ^4
'@img/sharp-darwin-arm64@0.34.5':
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [darwin]
'@img/sharp-darwin-x64@0.34.5':
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-darwin-arm64@1.2.4':
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
cpu: [arm64]
os: [darwin]
'@img/sharp-libvips-darwin-x64@1.2.4':
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
cpu: [x64]
os: [darwin]
'@img/sharp-libvips-linux-arm64@1.2.4':
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linux-arm@1.2.4':
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
cpu: [arm]
os: [linux]
'@img/sharp-libvips-linux-x64@1.2.4':
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
cpu: [x64]
os: [linux]
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
cpu: [arm64]
os: [linux]
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
cpu: [x64]
os: [linux]
'@img/sharp-linux-arm64@0.34.5':
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linux-arm@0.34.5':
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm]
os: [linux]
'@img/sharp-linux-x64@0.34.5':
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-linuxmusl-arm64@0.34.5':
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [linux]
'@img/sharp-linuxmusl-x64@0.34.5':
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [linux]
'@img/sharp-win32-arm64@0.34.5':
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [arm64]
os: [win32]
'@img/sharp-win32-x64@0.34.5':
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
cpu: [x64]
os: [win32]
'@modelcontextprotocol/sdk@1.27.1':
resolution: {integrity: sha512-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA==}
engines: {node: '>=18'}
peerDependencies:
'@cfworker/json-schema': ^4.1.1
zod: ^3.25 || ^4.0
peerDependenciesMeta:
'@cfworker/json-schema':
optional: true
'@types/node@22.19.15':
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
accepts@2.0.0:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
body-parser@2.2.2:
resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==}
engines: {node: '>=18'}
bytes@3.1.2:
resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==}
engines: {node: '>= 0.8'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
call-bound@1.0.4:
resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
engines: {node: '>= 0.4'}
content-disposition@1.0.1:
resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
engines: {node: '>=18'}
content-type@1.0.5:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'}
cookie-signature@1.2.2:
resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==}
engines: {node: '>=6.6.0'}
cookie@0.7.2:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
cors@2.8.6:
resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==}
engines: {node: '>= 0.10'}
cron-parser@5.5.0:
resolution: {integrity: sha512-oML4lKUXxizYswqmxuOCpgFS8BNUJpIu6k/2HVHyaL8Ynnf3wdf9tkns0yRdJLSIjkJ+b0DXHMZEHGpMwjnPww==}
engines: {node: '>=18'}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
debug@4.4.3:
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
depd@2.0.0:
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
engines: {node: '>= 0.8'}
dunder-proto@1.0.1:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
encodeurl@2.0.0:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
es-define-property@1.0.1:
resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
engines: {node: '>= 0.4'}
es-errors@1.3.0:
resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
engines: {node: '>= 0.4'}
es-object-atoms@1.1.1:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'}
escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
eventsource-parser@3.0.6:
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
engines: {node: '>=18.0.0'}
eventsource@3.0.7:
resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==}
engines: {node: '>=18.0.0'}
express-rate-limit@8.3.1:
resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==}
engines: {node: '>= 16'}
peerDependencies:
express: '>= 4.11'
express@5.2.1:
resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==}
engines: {node: '>= 18'}
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
finalhandler@2.1.1:
resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==}
engines: {node: '>= 18.0.0'}
forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'}
fresh@2.0.0:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
hono@4.12.8:
resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==}
engines: {node: '>=16.9.0'}
http-errors@2.0.1:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
iconv-lite@0.7.2:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
engines: {node: '>=0.10.0'}
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
ip-address@10.1.0:
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
engines: {node: '>= 12'}
ipaddr.js@1.9.1:
resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
engines: {node: '>= 0.10'}
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
jose@6.2.1:
resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==}
json-schema-traverse@1.0.0:
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
json-schema-typed@8.0.2:
resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==}
luxon@3.7.2:
resolution: {integrity: sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==}
engines: {node: '>=12'}
math-intrinsics@1.1.0:
resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
engines: {node: '>= 0.4'}
media-typer@1.1.0:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'}
merge-descriptors@2.0.0:
resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==}
engines: {node: '>=18'}
mime-db@1.54.0:
resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==}
engines: {node: '>= 0.6'}
mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
negotiator@1.0.0:
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
engines: {node: '>= 0.6'}
object-assign@4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
parseurl@1.3.3:
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
engines: {node: '>= 0.8'}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-to-regexp@8.3.0:
resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==}
pkce-challenge@5.0.1:
resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==}
engines: {node: '>=16.20.0'}
proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'}
qs@6.15.0:
resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==}
engines: {node: '>=0.6'}
range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
raw-body@3.0.2:
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
engines: {node: '>= 0.10'}
require-from-string@2.0.2:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
router@2.2.0:
resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==}
engines: {node: '>= 18'}
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
send@1.2.1:
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
engines: {node: '>= 18'}
serve-static@2.2.1:
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
engines: {node: '>= 18'}
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
side-channel-map@1.0.1:
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
engines: {node: '>= 0.4'}
side-channel-weakmap@1.0.2:
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
engines: {node: '>= 0.4'}
side-channel@1.1.0:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
statuses@2.0.2:
resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==}
engines: {node: '>= 0.8'}
toidentifier@1.0.1:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
type-is@2.0.1:
resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==}
engines: {node: '>= 0.6'}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
unpipe@1.0.0:
resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==}
engines: {node: '>= 0.8'}
vary@1.1.2:
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
engines: {node: '>= 0.8'}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
zod-to-json-schema@3.25.1:
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
peerDependencies:
zod: ^3.25 || ^4
zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
snapshots:
'@anthropic-ai/claude-agent-sdk@0.2.81(zod@4.3.6)':
dependencies:
zod: 4.3.6
optionalDependencies:
'@img/sharp-darwin-arm64': 0.34.5
'@img/sharp-darwin-x64': 0.34.5
'@img/sharp-linux-arm': 0.34.5
'@img/sharp-linux-arm64': 0.34.5
'@img/sharp-linux-x64': 0.34.5
'@img/sharp-linuxmusl-arm64': 0.34.5
'@img/sharp-linuxmusl-x64': 0.34.5
'@img/sharp-win32-arm64': 0.34.5
'@img/sharp-win32-x64': 0.34.5
'@hono/node-server@1.19.11(hono@4.12.8)':
dependencies:
hono: 4.12.8
'@img/sharp-darwin-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-arm64': 1.2.4
optional: true
'@img/sharp-darwin-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-darwin-x64': 1.2.4
optional: true
'@img/sharp-libvips-darwin-arm64@1.2.4':
optional: true
'@img/sharp-libvips-darwin-x64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linux-arm@1.2.4':
optional: true
'@img/sharp-libvips-linux-x64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
optional: true
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
optional: true
'@img/sharp-linux-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm64': 1.2.4
optional: true
'@img/sharp-linux-arm@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-arm': 1.2.4
optional: true
'@img/sharp-linux-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linux-x64': 1.2.4
optional: true
'@img/sharp-linuxmusl-arm64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
optional: true
'@img/sharp-linuxmusl-x64@0.34.5':
optionalDependencies:
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
optional: true
'@img/sharp-win32-arm64@0.34.5':
optional: true
'@img/sharp-win32-x64@0.34.5':
optional: true
'@modelcontextprotocol/sdk@1.27.1(zod@4.3.6)':
dependencies:
'@hono/node-server': 1.19.11(hono@4.12.8)
ajv: 8.18.0
ajv-formats: 3.0.1(ajv@8.18.0)
content-type: 1.0.5
cors: 2.8.6
cross-spawn: 7.0.6
eventsource: 3.0.7
eventsource-parser: 3.0.6
express: 5.2.1
express-rate-limit: 8.3.1(express@5.2.1)
hono: 4.12.8
jose: 6.2.1
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
raw-body: 3.0.2
zod: 4.3.6
zod-to-json-schema: 3.25.1(zod@4.3.6)
transitivePeerDependencies:
- supports-color
'@types/node@22.19.15':
dependencies:
undici-types: 6.21.0
accepts@2.0.0:
dependencies:
mime-types: 3.0.2
negotiator: 1.0.0
ajv-formats@3.0.1(ajv@8.18.0):
optionalDependencies:
ajv: 8.18.0
ajv@8.18.0:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.0
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
body-parser@2.2.2:
dependencies:
bytes: 3.1.2
content-type: 1.0.5
debug: 4.4.3
http-errors: 2.0.1
iconv-lite: 0.7.2
on-finished: 2.4.1
qs: 6.15.0
raw-body: 3.0.2
type-is: 2.0.1
transitivePeerDependencies:
- supports-color
bytes@3.1.2: {}
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
function-bind: 1.1.2
call-bound@1.0.4:
dependencies:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.3.0
content-disposition@1.0.1: {}
content-type@1.0.5: {}
cookie-signature@1.2.2: {}
cookie@0.7.2: {}
cors@2.8.6:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
cron-parser@5.5.0:
dependencies:
luxon: 3.7.2
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
debug@4.4.3:
dependencies:
ms: 2.1.3
depd@2.0.0: {}
dunder-proto@1.0.1:
dependencies:
call-bind-apply-helpers: 1.0.2
es-errors: 1.3.0
gopd: 1.2.0
ee-first@1.1.1: {}
encodeurl@2.0.0: {}
es-define-property@1.0.1: {}
es-errors@1.3.0: {}
es-object-atoms@1.1.1:
dependencies:
es-errors: 1.3.0
escape-html@1.0.3: {}
etag@1.8.1: {}
eventsource-parser@3.0.6: {}
eventsource@3.0.7:
dependencies:
eventsource-parser: 3.0.6
express-rate-limit@8.3.1(express@5.2.1):
dependencies:
express: 5.2.1
ip-address: 10.1.0
express@5.2.1:
dependencies:
accepts: 2.0.0
body-parser: 2.2.2
content-disposition: 1.0.1
content-type: 1.0.5
cookie: 0.7.2
cookie-signature: 1.2.2
debug: 4.4.3
depd: 2.0.0
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 2.1.1
fresh: 2.0.0
http-errors: 2.0.1
merge-descriptors: 2.0.0
mime-types: 3.0.2
on-finished: 2.4.1
once: 1.4.0
parseurl: 1.3.3
proxy-addr: 2.0.7
qs: 6.15.0
range-parser: 1.2.1
router: 2.2.0
send: 1.2.1
serve-static: 2.2.1
statuses: 2.0.2
type-is: 2.0.1
vary: 1.1.2
transitivePeerDependencies:
- supports-color
fast-deep-equal@3.1.3: {}
fast-uri@3.1.0: {}
finalhandler@2.1.1:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.2
transitivePeerDependencies:
- supports-color
forwarded@0.2.0: {}
fresh@2.0.0: {}
function-bind@1.1.2: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
es-define-property: 1.0.1
es-errors: 1.3.0
es-object-atoms: 1.1.1
function-bind: 1.1.2
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
math-intrinsics: 1.1.0
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
es-object-atoms: 1.1.1
gopd@1.2.0: {}
has-symbols@1.1.0: {}
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
hono@4.12.8: {}
http-errors@2.0.1:
dependencies:
depd: 2.0.0
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 2.0.2
toidentifier: 1.0.1
iconv-lite@0.7.2:
dependencies:
safer-buffer: 2.1.2
inherits@2.0.4: {}
ip-address@10.1.0: {}
ipaddr.js@1.9.1: {}
is-promise@4.0.0: {}
isexe@2.0.0: {}
jose@6.2.1: {}
json-schema-traverse@1.0.0: {}
json-schema-typed@8.0.2: {}
luxon@3.7.2: {}
math-intrinsics@1.1.0: {}
media-typer@1.1.0: {}
merge-descriptors@2.0.0: {}
mime-db@1.54.0: {}
mime-types@3.0.2:
dependencies:
mime-db: 1.54.0
ms@2.1.3: {}
negotiator@1.0.0: {}
object-assign@4.1.1: {}
object-inspect@1.13.4: {}
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
once@1.4.0:
dependencies:
wrappy: 1.0.2
parseurl@1.3.3: {}
path-key@3.1.1: {}
path-to-regexp@8.3.0: {}
pkce-challenge@5.0.1: {}
proxy-addr@2.0.7:
dependencies:
forwarded: 0.2.0
ipaddr.js: 1.9.1
qs@6.15.0:
dependencies:
side-channel: 1.1.0
range-parser@1.2.1: {}
raw-body@3.0.2:
dependencies:
bytes: 3.1.2
http-errors: 2.0.1
iconv-lite: 0.7.2
unpipe: 1.0.0
require-from-string@2.0.2: {}
router@2.2.0:
dependencies:
debug: 4.4.3
depd: 2.0.0
is-promise: 4.0.0
parseurl: 1.3.3
path-to-regexp: 8.3.0
transitivePeerDependencies:
- supports-color
safer-buffer@2.1.2: {}
send@1.2.1:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
fresh: 2.0.0
http-errors: 2.0.1
mime-types: 3.0.2
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.2
transitivePeerDependencies:
- supports-color
serve-static@2.2.1:
dependencies:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
send: 1.2.1
transitivePeerDependencies:
- supports-color
setprototypeof@1.2.0: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
side-channel-map@1.0.1:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-weakmap@1.0.2:
dependencies:
call-bound: 1.0.4
es-errors: 1.3.0
get-intrinsic: 1.3.0
object-inspect: 1.13.4
side-channel-map: 1.0.1
side-channel@1.1.0:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
side-channel-list: 1.0.0
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
statuses@2.0.2: {}
toidentifier@1.0.1: {}
type-is@2.0.1:
dependencies:
content-type: 1.0.5
media-typer: 1.1.0
mime-types: 3.0.2
typescript@5.9.3: {}
undici-types@6.21.0: {}
unpipe@1.0.0: {}
vary@1.1.2: {}
which@2.0.2:
dependencies:
isexe: 2.0.0
wrappy@1.0.2: {}
zod-to-json-schema@3.25.1(zod@4.3.6):
dependencies:
zod: 4.3.6
zod@4.3.6: {}

View File

@@ -137,9 +137,6 @@ function createExecutableWrapperDir(baseEnv: NodeJS.ProcessEnv): string {
const candidateRoots = [
baseEnv.EJCLAW_REVIEWER_GIT_WRAPPER_ROOT,
baseEnv.HOME ? path.join(baseEnv.HOME, '.ejclaw-reviewer-runtime') : null,
process.cwd()
? path.join(process.cwd(), '.ejclaw-reviewer-runtime')
: null,
path.join(os.tmpdir(), '.ejclaw-reviewer-runtime'),
].filter((value): value is string => Boolean(value));

View File

@@ -175,7 +175,7 @@ describe('claude reviewer runtime guard', () => {
it('prefers an executable HOME-scoped wrapper dir before tmp', () => {
const homeDir = fs.mkdtempSync(
path.join(process.cwd(), '.ejclaw-reviewer-home-'),
path.join(os.tmpdir(), 'ejclaw-reviewer-home-'),
);
try {
const env = buildReviewerGitGuardEnv(

View File

@@ -1,172 +0,0 @@
{
"name": "ejclaw-codex-runner",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ejclaw-codex-runner",
"version": "1.0.0",
"dependencies": {
"@openai/codex": "^0.115.0"
},
"devDependencies": {
"@types/node": "^22.10.7",
"typescript": "^5.7.3"
}
},
"node_modules/@openai/codex": {
"version": "0.115.0",
"resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0.tgz",
"integrity": "sha512-uu689DHUzvuPcb39hJ+7fqy++TAvY32w9VggDpcz3HS0Sx0WadWoAPPcMK547P2T6AqfMsAtA8kspkR3tqErOg==",
"license": "Apache-2.0",
"bin": {
"codex": "bin/codex.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"@openai/codex-darwin-arm64": "npm:@openai/codex@0.115.0-darwin-arm64",
"@openai/codex-darwin-x64": "npm:@openai/codex@0.115.0-darwin-x64",
"@openai/codex-linux-arm64": "npm:@openai/codex@0.115.0-linux-arm64",
"@openai/codex-linux-x64": "npm:@openai/codex@0.115.0-linux-x64",
"@openai/codex-win32-arm64": "npm:@openai/codex@0.115.0-win32-arm64",
"@openai/codex-win32-x64": "npm:@openai/codex@0.115.0-win32-x64"
}
},
"node_modules/@openai/codex-darwin-arm64": {
"name": "@openai/codex",
"version": "0.115.0-darwin-arm64",
"resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0-darwin-arm64.tgz",
"integrity": "sha512-wTWV+YlDTL0y0mL+FMmbzhilm+dPkbIZTe/lH3LwBzcEMhgSGLsPdZLfAiMND4wFE21HS7H0pjMogNQEMLQmqg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16"
}
},
"node_modules/@openai/codex-darwin-x64": {
"name": "@openai/codex",
"version": "0.115.0-darwin-x64",
"resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0-darwin-x64.tgz",
"integrity": "sha512-EPzgymU4CFp83qjv29wXFwhWib3zC8g6SLTJGh2OpcJiOYyLY0bO53FB+QchL1jC9gm1uLyD5j5F3ddI0AbjIg==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=16"
}
},
"node_modules/@openai/codex-linux-arm64": {
"name": "@openai/codex",
"version": "0.115.0-linux-arm64",
"resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0-linux-arm64.tgz",
"integrity": "sha512-40+SCyI+LvVx/iX30qH7dTQzWt9MZxDaK2E6YRB4hoYej5UALrhkFNzweHa5uvqvhmqGZCuagZOYB8285eGCgw==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16"
}
},
"node_modules/@openai/codex-linux-x64": {
"name": "@openai/codex",
"version": "0.115.0-linux-x64",
"resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0-linux-x64.tgz",
"integrity": "sha512-+6eRd2p4KMrhQvuF7XpjYIdCA2Ok2LbhOq+ywZdLpHbmwQ/Yynd0gJ/Q90xCeo2vloCwl9WsbsiVVSahG5FyWg==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=16"
}
},
"node_modules/@openai/codex-win32-arm64": {
"name": "@openai/codex",
"version": "0.115.0-win32-arm64",
"resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0-win32-arm64.tgz",
"integrity": "sha512-yaYhQ0kPL9Kithmrr1vh5A4c+gqAMhMZeA/htTtkpWW8RQkmwgcYYpX/Ky2cEzu0w51pvxQQy63LgHftc+pg1Q==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16"
}
},
"node_modules/@openai/codex-win32-x64": {
"name": "@openai/codex",
"version": "0.115.0-win32-x64",
"resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.115.0-win32-x64.tgz",
"integrity": "sha512-wtYf2HJCB+p+Uj7o1W08fRgZMzKCVGRQ4YdSfLRNfF4wwPqX5XsK9blsv7brukn5J9lclYxagiR6qGvbfHbD7w==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=16"
}
},
"node_modules/@types/node": {
"version": "22.19.15",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz",
"integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

View File

@@ -3,6 +3,7 @@
"version": "1.0.0",
"type": "module",
"description": "Codex app-server runner for EJClaw",
"packageManager": "bun@1.3.11",
"main": "dist/index.js",
"scripts": {
"build": "tsc",

View File

@@ -1,111 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
'@openai/codex':
specifier: ^0.115.0
version: 0.115.0
devDependencies:
'@types/node':
specifier: ^22.10.7
version: 22.19.15
typescript:
specifier: ^5.7.3
version: 5.9.3
packages:
'@openai/codex@0.115.0':
resolution: {integrity: sha512-uu689DHUzvuPcb39hJ+7fqy++TAvY32w9VggDpcz3HS0Sx0WadWoAPPcMK547P2T6AqfMsAtA8kspkR3tqErOg==}
engines: {node: '>=16'}
hasBin: true
'@openai/codex@0.115.0-darwin-arm64':
resolution: {integrity: sha512-wTWV+YlDTL0y0mL+FMmbzhilm+dPkbIZTe/lH3LwBzcEMhgSGLsPdZLfAiMND4wFE21HS7H0pjMogNQEMLQmqg==}
engines: {node: '>=16'}
cpu: [arm64]
os: [darwin]
'@openai/codex@0.115.0-darwin-x64':
resolution: {integrity: sha512-EPzgymU4CFp83qjv29wXFwhWib3zC8g6SLTJGh2OpcJiOYyLY0bO53FB+QchL1jC9gm1uLyD5j5F3ddI0AbjIg==}
engines: {node: '>=16'}
cpu: [x64]
os: [darwin]
'@openai/codex@0.115.0-linux-arm64':
resolution: {integrity: sha512-40+SCyI+LvVx/iX30qH7dTQzWt9MZxDaK2E6YRB4hoYej5UALrhkFNzweHa5uvqvhmqGZCuagZOYB8285eGCgw==}
engines: {node: '>=16'}
cpu: [arm64]
os: [linux]
'@openai/codex@0.115.0-linux-x64':
resolution: {integrity: sha512-+6eRd2p4KMrhQvuF7XpjYIdCA2Ok2LbhOq+ywZdLpHbmwQ/Yynd0gJ/Q90xCeo2vloCwl9WsbsiVVSahG5FyWg==}
engines: {node: '>=16'}
cpu: [x64]
os: [linux]
'@openai/codex@0.115.0-win32-arm64':
resolution: {integrity: sha512-yaYhQ0kPL9Kithmrr1vh5A4c+gqAMhMZeA/htTtkpWW8RQkmwgcYYpX/Ky2cEzu0w51pvxQQy63LgHftc+pg1Q==}
engines: {node: '>=16'}
cpu: [arm64]
os: [win32]
'@openai/codex@0.115.0-win32-x64':
resolution: {integrity: sha512-wtYf2HJCB+p+Uj7o1W08fRgZMzKCVGRQ4YdSfLRNfF4wwPqX5XsK9blsv7brukn5J9lclYxagiR6qGvbfHbD7w==}
engines: {node: '>=16'}
cpu: [x64]
os: [win32]
'@types/node@22.19.15':
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
hasBin: true
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
snapshots:
'@openai/codex@0.115.0':
optionalDependencies:
'@openai/codex-darwin-arm64': '@openai/codex@0.115.0-darwin-arm64'
'@openai/codex-darwin-x64': '@openai/codex@0.115.0-darwin-x64'
'@openai/codex-linux-arm64': '@openai/codex@0.115.0-linux-arm64'
'@openai/codex-linux-x64': '@openai/codex@0.115.0-linux-x64'
'@openai/codex-win32-arm64': '@openai/codex@0.115.0-win32-arm64'
'@openai/codex-win32-x64': '@openai/codex@0.115.0-win32-x64'
'@openai/codex@0.115.0-darwin-arm64':
optional: true
'@openai/codex@0.115.0-darwin-x64':
optional: true
'@openai/codex@0.115.0-linux-arm64':
optional: true
'@openai/codex@0.115.0-linux-x64':
optional: true
'@openai/codex@0.115.0-win32-arm64':
optional: true
'@openai/codex@0.115.0-win32-x64':
optional: true
'@types/node@22.19.15':
dependencies:
undici-types: 6.21.0
typescript@5.9.3: {}
undici-types@6.21.0: {}

View File

@@ -49,9 +49,6 @@ function createExecutableWrapperDir(baseEnv: NodeJS.ProcessEnv): string {
const candidateRoots = [
baseEnv.EJCLAW_REVIEWER_GIT_WRAPPER_ROOT,
baseEnv.HOME ? path.join(baseEnv.HOME, '.ejclaw-reviewer-runtime') : null,
process.cwd()
? path.join(process.cwd(), '.ejclaw-reviewer-runtime')
: null,
path.join(os.tmpdir(), '.ejclaw-reviewer-runtime'),
].filter((value): value is string => Boolean(value));

View File

@@ -79,7 +79,7 @@ describe('codex reviewer runtime guard', () => {
it('prefers an executable HOME-scoped wrapper dir before tmp', () => {
const homeDir = fs.mkdtempSync(
path.join(process.cwd(), '.ejclaw-reviewer-home-'),
path.join(os.tmpdir(), 'ejclaw-reviewer-home-'),
);
try {
const env = buildReviewerGitGuardEnv(

View File

@@ -1,123 +0,0 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, describe, expect, it, vi } from 'vitest';
const { execSyncMock } = vi.hoisted(() => ({
execSyncMock: vi.fn(),
}));
vi.mock('child_process', () => ({
execSync: execSyncMock,
}));
import {
detectLegacyServiceIssues,
formatLegacyServiceFailureMessage,
} from './legacy-service-guard.js';
describe('legacy service guard', () => {
const tempRoots: string[] = [];
afterEach(() => {
execSyncMock.mockReset();
for (const root of tempRoots.splice(0)) {
fs.rmSync(root, { recursive: true, force: true });
}
});
it('detects legacy systemd units in the opposite scope', () => {
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-legacy-'));
tempRoots.push(projectRoot);
execSyncMock.mockImplementation((cmd: string) => {
if (cmd === 'systemctl is-active ejclaw-codex') {
throw new Error('inactive');
}
if (cmd === 'systemctl list-unit-files') {
return 'ejclaw-codex.service enabled\n';
}
if (cmd === 'systemctl --user is-active ejclaw-codex') {
throw new Error('inactive');
}
if (cmd === 'systemctl --user list-unit-files') {
return '';
}
if (cmd === 'systemctl is-active ejclaw-review') {
throw new Error('inactive');
}
if (cmd === 'systemctl --user is-active ejclaw-review') {
throw new Error('inactive');
}
if (cmd === 'systemctl list-unit-files' || cmd === 'systemctl --user list-unit-files') {
return '';
}
throw new Error(`unexpected command: ${cmd}`);
});
expect(detectLegacyServiceIssues(projectRoot, 'systemd')).toEqual([
{
name: 'ejclaw-codex',
status: 'stopped',
sources: ['systemd-system'],
},
]);
});
it('detects legacy nohup artifacts even on systemd hosts', () => {
const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-legacy-'));
tempRoots.push(projectRoot);
fs.writeFileSync(path.join(projectRoot, 'start-ejclaw-codex.sh'), '#!/bin/bash\n');
execSyncMock.mockImplementation((cmd: string) => {
if (
cmd === 'systemctl is-active ejclaw-codex' ||
cmd === 'systemctl --user is-active ejclaw-codex' ||
cmd === 'systemctl is-active ejclaw-review' ||
cmd === 'systemctl --user is-active ejclaw-review'
) {
throw new Error('inactive');
}
if (
cmd === 'systemctl list-unit-files' ||
cmd === 'systemctl --user list-unit-files'
) {
return '';
}
throw new Error(`unexpected command: ${cmd}`);
});
expect(detectLegacyServiceIssues(projectRoot, 'systemd')).toEqual([
{
name: 'ejclaw-codex',
status: 'stopped',
sources: ['nohup'],
},
]);
});
it('formats cleanup instructions for the detected scopes only', () => {
const message = formatLegacyServiceFailureMessage({
projectRoot: '/srv/ejclaw',
serviceManager: 'systemd',
homeDir: '/home/user',
services: [
{
name: 'ejclaw-codex',
status: 'stopped',
sources: ['systemd-system', 'nohup'],
},
{
name: 'ejclaw-review',
status: 'running',
sources: ['systemd-user'],
},
],
});
expect(message).toContain('systemctl disable --now ejclaw-codex');
expect(message).toContain('systemctl --user disable --now ejclaw-review');
expect(message).toContain('pkill -F "/srv/ejclaw/ejclaw-codex.pid"');
expect(message).not.toContain('systemctl --user disable --now ejclaw-codex');
});
});

View File

@@ -1,209 +0,0 @@
import os from 'os';
import path from 'path';
import type { ServiceManager } from './platform.js';
import { getLegacyServiceDefs } from './service-defs.js';
import {
checkLaunchdServiceArtifact,
checkNohupServiceArtifact,
checkSystemdServiceInScope,
type ServiceCheck,
type SystemdScope,
} from './verify-services.js';
type LegacySource = 'launchd' | 'systemd-system' | 'systemd-user' | 'nohup';
export interface LegacyServiceIssue extends ServiceCheck {
sources: LegacySource[];
}
function summarizeLegacyStatus(statuses: ServiceCheck['status'][]): ServiceCheck['status'] {
if (statuses.includes('running')) {
return 'running';
}
if (statuses.includes('stopped')) {
return 'stopped';
}
return 'not_found';
}
export function detectLegacyServiceIssues(
projectRoot: string,
serviceManager: ServiceManager,
homeDir = os.homedir(),
): LegacyServiceIssue[] {
return getLegacyServiceDefs(projectRoot)
.map((def) => {
if (serviceManager === 'launchd') {
const status = checkLaunchdServiceArtifact(
def.launchdLabel,
path.join(homeDir, 'Library', 'LaunchAgents', `${def.launchdLabel}.plist`),
);
if (status === 'not_found') return null;
return {
name: def.name,
status,
sources: ['launchd'] as LegacySource[],
};
}
if (serviceManager === 'systemd') {
const scopedChecks: Array<{ source: LegacySource; status: ServiceCheck['status'] }> = [
{
source: 'systemd-system',
status: checkSystemdServiceInScope(def.name, 'system'),
},
{
source: 'systemd-user',
status: checkSystemdServiceInScope(def.name, 'user'),
},
{
source: 'nohup',
status: checkNohupServiceArtifact(projectRoot, def.name),
},
];
const detected = scopedChecks.filter(
(current) => current.status !== 'not_found',
);
if (detected.length === 0) return null;
return {
name: def.name,
status: summarizeLegacyStatus(detected.map((current) => current.status)),
sources: detected.map((current) => current.source),
};
}
const status = checkNohupServiceArtifact(projectRoot, def.name);
if (status === 'not_found') return null;
return {
name: def.name,
status,
sources: ['nohup'] as LegacySource[],
};
})
.filter((service): service is LegacyServiceIssue => Boolean(service));
}
function formatSystemdCleanupForScope(
serviceNames: string[],
homeDir: string,
scope: SystemdScope,
): string[] {
if (serviceNames.length === 0) return [];
const systemctlPrefix = scope === 'system' ? 'systemctl' : 'systemctl --user';
const unitDir =
scope === 'system'
? '/etc/systemd/system'
: path.join(homeDir, '.config', 'systemd', 'user');
const unitPaths = serviceNames
.map((serviceName) =>
JSON.stringify(path.join(unitDir, `${serviceName}.service`)),
)
.join(' ');
return [
`${systemctlPrefix} disable --now ${serviceNames.join(' ')}`,
`rm -f ${unitPaths}`,
`${systemctlPrefix} daemon-reload`,
];
}
function formatLaunchdCleanup(serviceNames: string[], homeDir: string): string {
if (serviceNames.length === 0) {
return '';
}
const plistPaths = serviceNames.map((serviceName) => {
const label = serviceName === 'ejclaw-codex'
? 'com.ejclaw-codex'
: 'com.ejclaw-review';
return path.join(homeDir, 'Library', 'LaunchAgents', `${label}.plist`);
});
return [
...plistPaths.map(
(plistPath) =>
`launchctl unload ${JSON.stringify(plistPath)} 2>/dev/null || true`,
),
`rm -f ${plistPaths.map((plistPath) => JSON.stringify(plistPath)).join(' ')}`,
].join('\n');
}
function formatNohupCleanup(
projectRoot: string,
serviceNames: string[],
): string {
if (serviceNames.length === 0) {
return '';
}
const pidPaths = serviceNames
.map((serviceName) => path.join(projectRoot, `${serviceName}.pid`))
.map((currentPath) => JSON.stringify(currentPath));
const wrapperPaths = serviceNames
.map((serviceName) => path.join(projectRoot, `start-${serviceName}.sh`))
.map((currentPath) => JSON.stringify(currentPath));
return [
...pidPaths.map((pidPath) => `pkill -F ${pidPath} 2>/dev/null || true`),
`rm -f ${[...pidPaths, ...wrapperPaths].join(' ')}`,
].join('\n');
}
export function formatLegacyServiceFailureMessage(args: {
projectRoot: string;
serviceManager: ServiceManager;
services: LegacyServiceIssue[];
homeDir?: string;
}): string {
const homeDir = args.homeDir ?? os.homedir();
const details = args.services
.map(
(service) =>
`${service.name}=${service.status} [${service.sources.join(',')}]`,
)
.join(', ');
let cleanupLines: string[] = [];
if (args.serviceManager === 'launchd') {
cleanupLines = formatLaunchdCleanup(
args.services
.filter((service) => service.sources.includes('launchd'))
.map((service) => service.name),
homeDir,
).split('\n');
} else if (args.serviceManager === 'systemd') {
cleanupLines = [
...formatSystemdCleanupForScope(
args.services
.filter((service) => service.sources.includes('systemd-system'))
.map((service) => service.name),
homeDir,
'system',
),
...formatSystemdCleanupForScope(
args.services
.filter((service) => service.sources.includes('systemd-user'))
.map((service) => service.name),
homeDir,
'user',
),
...formatNohupCleanup(
args.projectRoot,
args.services
.filter((service) => service.sources.includes('nohup'))
.map((service) => service.name),
).split('\n'),
].filter(Boolean);
} else {
cleanupLines = formatNohupCleanup(
args.projectRoot,
args.services.map((service) => service.name),
).split('\n');
}
return [
`Legacy EJClaw multi-service install detected: ${details}`,
'This setup is reinstall-only. Remove the legacy services before continuing.',
'Suggested cleanup:',
...cleanupLines,
].join('\n');
}

View File

@@ -82,31 +82,6 @@ describe('restartStackServices', () => {
).toThrow('restart:stack only supports Linux systemd services in this repo');
});
it('fails fast when legacy services are still present', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-restart-'));
tempRoots.push(tempRoot);
const execFileSyncImpl = vi.fn();
expect(() =>
restartStackServices(tempRoot, {
direct: true,
execFileSyncImpl,
runningAsRoot: false,
serviceManager: 'systemd',
serviceId: null,
legacyServiceIssues: [
{
name: 'ejclaw-codex',
status: 'stopped',
sources: ['systemd-system'],
},
],
}),
).toThrow('Legacy EJClaw multi-service install detected');
expect(execFileSyncImpl).not.toHaveBeenCalled();
});
it('falls back to direct restart when the oneshot unit is not installed for an external caller', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-restart-'));
tempRoots.push(tempRoot);

View File

@@ -1,12 +1,6 @@
import { execFileSync } from 'child_process';
import os from 'os';
import { pathToFileURL } from 'url';
import {
detectLegacyServiceIssues,
formatLegacyServiceFailureMessage,
type LegacyServiceIssue,
} from './legacy-service-guard.js';
import { getServiceManager, isRoot } from './platform.js';
import { getConfiguredServiceNames } from './service-defs.js';
@@ -23,8 +17,6 @@ interface RestartStackDeps {
serviceManager?: ServiceManager;
direct?: boolean;
serviceId?: string | null;
legacyServiceIssues?: LegacyServiceIssue[];
homeDir?: string;
}
function restartStackServicesDirect(
@@ -70,24 +62,6 @@ export function restartStackServices(
);
}
const legacyServiceIssues =
deps.legacyServiceIssues ??
detectLegacyServiceIssues(
projectRoot,
serviceManager,
deps.homeDir ?? os.homedir(),
);
if (legacyServiceIssues.length > 0) {
throw new Error(
formatLegacyServiceFailureMessage({
projectRoot,
serviceManager,
services: legacyServiceIssues,
homeDir: deps.homeDir,
}),
);
}
const services = getConfiguredServiceNames(projectRoot);
const execImpl = deps.execFileSyncImpl ?? execFileSync;
const systemctlArgs = deps.runningAsRoot ?? isRoot() ? [] : ['--user'];

View File

@@ -1,4 +1,4 @@
export type ServiceKind = 'primary' | 'legacy';
export type ServiceKind = 'primary';
export interface ServiceDef {
/** Stable topology kind used by setup/verify logic */
@@ -35,23 +35,6 @@ const CURRENT_SERVICE_TEMPLATES: ServiceTemplate[] = [
},
];
const LEGACY_SERVICE_TEMPLATES: ServiceTemplate[] = [
{
kind: 'legacy',
name: 'ejclaw-codex',
launchdLabel: 'com.ejclaw-codex',
description: 'Legacy EJClaw Codex Assistant',
logName: 'ejclaw-codex',
},
{
kind: 'legacy',
name: 'ejclaw-review',
launchdLabel: 'com.ejclaw-review',
description: 'Legacy EJClaw Codex Review Assistant',
logName: 'ejclaw-review',
},
];
function materializeServiceDef(template: ServiceTemplate): ServiceDef {
const environmentFile = undefined;
const extraEnv =
@@ -79,17 +62,6 @@ export function getServiceDefs(projectRoot: string): ServiceDef[] {
);
}
export function getLegacyServiceDefs(projectRoot: string): ServiceDef[] {
void projectRoot;
return LEGACY_SERVICE_TEMPLATES.map((template) =>
materializeServiceDef(template),
);
}
export function getConfiguredServiceNames(projectRoot: string): string[] {
return getServiceDefs(projectRoot).map((def) => def.name);
}
export function getLegacyServiceNames(projectRoot: string): string[] {
return getLegacyServiceDefs(projectRoot).map((def) => def.name);
}

View File

@@ -9,11 +9,7 @@ import {
buildStackRestartSystemdUnit,
buildSystemdUnit,
} from './service-renderers.js';
import {
getLegacyServiceDefs,
getServiceDefs,
type ServiceDef,
} from './service-defs.js';
import { getServiceDefs, type ServiceDef } from './service-defs.js';
/**
* Tests for service configuration generation.
@@ -189,19 +185,6 @@ describe('service definitions', () => {
});
});
it('keeps legacy service identities available for migration guards', () => {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ejclaw-stack-'));
tempRoots.push(tempRoot);
const defs = getLegacyServiceDefs(tempRoot);
expect(defs.map((def) => def.name)).toEqual([
'ejclaw-codex',
'ejclaw-review',
]);
expect(defs.map((def) => def.kind)).toEqual(['legacy', 'legacy']);
});
it('generates a oneshot stack restart unit', () => {
const unit = buildStackRestartSystemdUnit(
'/srv/ejclaw',

View File

@@ -17,10 +17,6 @@ import {
getNodePath,
getServiceManager,
} from './platform.js';
import {
detectLegacyServiceIssues,
formatLegacyServiceFailureMessage,
} from './legacy-service-guard.js';
import { getServiceDefs } from './service-defs.js';
import { setupLaunchd, setupLinux } from './service-installers.js';
import { emitStatus } from './status.js';
@@ -54,37 +50,6 @@ export async function run(_args: string[]): Promise<void> {
);
}
const legacyServiceIssues = detectLegacyServiceIssues(
projectRoot,
serviceManager,
homeDir,
);
if (legacyServiceIssues.length > 0) {
const errorMessage = formatLegacyServiceFailureMessage({
projectRoot,
serviceManager,
homeDir,
services: legacyServiceIssues,
});
logger.error(
{ legacyServiceIssues, serviceManager },
'Legacy multi-service install detected during setup',
);
emitStatus('SETUP_SERVICE', {
SERVICE_TYPE: serviceManager,
NODE_PATH: nodePath,
PROJECT_PATH: projectRoot,
STATUS: 'failed',
ERROR: 'legacy_services_detected',
LEGACY_SERVICES: legacyServiceIssues
.map((service) => `${service.name}:${service.status}`)
.join(','),
LOG: 'logs/setup.log',
});
console.error(errorMessage);
process.exit(1);
}
// Build first
logger.info('Building TypeScript');
try {

View File

@@ -7,13 +7,7 @@
*
* Uses better-sqlite3 directly (no sqlite3 CLI), platform-aware service checks.
*/
import os from 'os';
import { logger } from '../src/logger.js';
import {
detectLegacyServiceIssues,
formatLegacyServiceFailureMessage,
} from './legacy-service-guard.js';
import { getServiceManager } from './platform.js';
import { getServiceDefs } from './service-defs.js';
import { emitStatus } from './status.js';
@@ -39,21 +33,10 @@ export async function run(_args: string[]): Promise<void> {
// 1. Check service statuses
const serviceDefs = getServiceDefs(projectRoot);
const services = getServiceChecks(serviceDefs, projectRoot, mgr);
const legacyServiceIssues = detectLegacyServiceIssues(
projectRoot,
mgr,
os.homedir(),
);
for (const svc of services) {
logger.info({ service: svc.name, status: svc.status }, 'Service status');
}
for (const svc of legacyServiceIssues) {
logger.error(
{ service: svc.name, status: svc.status },
'Legacy service detected during verification',
);
}
const credentials = detectCredentials(projectRoot);
const channelAuth = detectChannelAuth();
@@ -80,10 +63,7 @@ export async function run(_args: string[]): Promise<void> {
activeArbiterTasks,
},
);
const legacyServicesSummary = Object.fromEntries(
legacyServiceIssues.map((service) => [service.name, service.status]),
);
const status = legacyServiceIssues.length > 0 ? 'failed' : baseStatus;
const status = baseStatus;
logger.info(
{
@@ -92,27 +72,12 @@ export async function run(_args: string[]): Promise<void> {
tribunalRooms: detectedTribunalRooms,
activeArbiterTasks: detectedActiveArbiterTasks,
servicesSummary,
legacyServicesSummary,
},
'Verification complete',
);
if (legacyServiceIssues.length > 0) {
logger.error(
{
cleanup: formatLegacyServiceFailureMessage({
projectRoot,
serviceManager: mgr,
homeDir: os.homedir(),
services: legacyServiceIssues,
}),
},
'Verification failed due to legacy multi-service install',
);
}
emitStatus('VERIFY', {
SERVICES: JSON.stringify(servicesSummary),
LEGACY_SERVICES: JSON.stringify(legacyServicesSummary),
// Legacy field (keep for backward compatibility)
SERVICE: services[0].status,
CREDENTIALS: credentials,

View File

@@ -7,6 +7,3 @@ export declare function isVerificationSnapshotExcludedPath(
currentPath: string,
): boolean;
export declare function computeVerificationSnapshotId(repoDir: string): string;
export declare function resolveVerificationResponsesDir(
hostIpcDir: string,
): string;

View File

@@ -77,7 +77,3 @@ export function computeVerificationSnapshotId(repoDir) {
updateVerificationSnapshotHash(hash, repoDir, repoDir);
return `fs:${hash.digest('hex').slice(0, 24)}`;
}
export function resolveVerificationResponsesDir(hostIpcDir) {
return path.join(hostIpcDir, 'verification-responses');
}

View File

@@ -67,7 +67,7 @@ export function enqueueGenericFollowUpAfterDeliveryRetry(args: {
deliveryRole: args.deliveryRole,
pendingTaskStatus: args.pendingTask?.status ?? null,
},
'Skipping queued follow-up after reviewer merge_ready delivery because inline finalize will handle the handoff',
'No queued follow-up was required after delivery retry',
);
return;
}
@@ -90,6 +90,21 @@ export function enqueueGenericFollowUpAfterDeliveryRetry(args: {
nextTurnAction.kind,
args.runId,
);
if (scheduled) {
args.log.info(
{
workItemId: args.workItemId,
chatJid: args.chatJid,
deliveryRole: args.deliveryRole,
taskId: args.pendingTask?.id ?? null,
pendingTaskStatus: args.pendingTask?.status ?? null,
taskStatus: args.pendingTask?.status ?? null,
intentKind: nextTurnAction.kind,
},
'Queued paired follow-up after delivery retry',
);
return;
}
if (!scheduled) {
args.log.info(
{
@@ -97,6 +112,7 @@ export function enqueueGenericFollowUpAfterDeliveryRetry(args: {
chatJid: args.chatJid,
deliveryRole: args.deliveryRole,
taskId: args.pendingTask?.id ?? null,
pendingTaskStatus: args.pendingTask?.status ?? null,
taskStatus: args.pendingTask?.status ?? null,
intentKind: nextTurnAction.kind,
},

View File

@@ -105,6 +105,39 @@ describe('message-runtime-rules', () => {
).toEqual({ kind: 'none' });
});
it('dispatches reviewer and arbiter delivery success through paired follow-up enqueue when a handoff is pending', () => {
expect(
resolveFollowUpDispatch({
source: 'delivery-success',
nextTurnAction: { kind: 'owner-follow-up' },
completedRole: 'reviewer',
}),
).toEqual({
kind: 'enqueue',
queueKind: 'paired-follow-up',
});
expect(
resolveFollowUpDispatch({
source: 'delivery-success',
nextTurnAction: { kind: 'finalize-owner-turn' },
completedRole: 'reviewer',
}),
).toEqual({
kind: 'enqueue',
queueKind: 'paired-follow-up',
});
expect(
resolveFollowUpDispatch({
source: 'delivery-success',
nextTurnAction: { kind: 'owner-follow-up' },
completedRole: 'arbiter',
}),
).toEqual({
kind: 'enqueue',
queueKind: 'paired-follow-up',
});
});
it('dispatches delivery retry follow-ups through either generic message checks or paired follow-up enqueue', () => {
expect(
resolveFollowUpDispatch({
@@ -132,7 +165,10 @@ describe('message-runtime-rules', () => {
nextTurnAction: { kind: 'finalize-owner-turn' },
completedRole: 'reviewer',
}),
).toEqual({ kind: 'none' });
).toEqual({
kind: 'enqueue',
queueKind: 'paired-follow-up',
});
});
it('dispatches bot-only follow-ups through inline finalize or paired enqueue', () => {

View File

@@ -118,6 +118,7 @@ export function resolveNextTurnAction(args: {
export function resolveFollowUpDispatch(args: {
source:
| 'delivery-success'
| 'owner-delivery-success'
| 'delivery-retry'
| 'bot-only-follow-up'
@@ -128,18 +129,21 @@ export function resolveFollowUpDispatch(args: {
sawOutput?: boolean;
}): FollowUpDispatch {
switch (args.source) {
case 'delivery-success':
case 'owner-delivery-success':
return args.nextTurnAction.kind === 'reviewer-turn'
? { kind: 'enqueue', queueKind: 'paired-follow-up' }
: { kind: 'none' };
if (
args.source === 'owner-delivery-success' ||
args.completedRole === 'owner'
) {
return args.nextTurnAction.kind === 'reviewer-turn'
? { kind: 'enqueue', queueKind: 'paired-follow-up' }
: { kind: 'none' };
}
return args.nextTurnAction.kind === 'none'
? { kind: 'none' }
: { kind: 'enqueue', queueKind: 'paired-follow-up' };
case 'delivery-retry':
if (
args.completedRole === 'reviewer' &&
args.nextTurnAction.kind === 'finalize-owner-turn'
) {
return { kind: 'none' };
}
if (args.nextTurnAction.kind === 'none') {
return { kind: 'enqueue', queueKind: 'message-check' };
}

View File

@@ -882,15 +882,17 @@ describe('createMessageRuntime', () => {
'reviewer final retry',
);
expect(ownerChannel.sendMessage).not.toHaveBeenCalled();
expect(enqueueMessageCheck).not.toHaveBeenCalled();
expect(enqueueMessageCheck).toHaveBeenCalledWith(chatJid);
expect(logger.info).toHaveBeenCalledWith(
expect.objectContaining({
workItemId: 101,
chatJid,
deliveryRole: 'reviewer',
taskId: 'task-review-delivery-role',
pendingTaskStatus: 'merge_ready',
intentKind: 'finalize-owner-turn',
}),
'Skipping queued follow-up after reviewer merge_ready delivery because inline finalize will handle the handoff',
'Queued paired follow-up after delivery retry',
);
});
@@ -975,15 +977,17 @@ describe('createMessageRuntime', () => {
'fallback reviewer final retry',
);
expect(ownerChannel.sendMessage).not.toHaveBeenCalled();
expect(enqueueMessageCheck).not.toHaveBeenCalled();
expect(enqueueMessageCheck).toHaveBeenCalledWith(chatJid);
expect(logger.info).toHaveBeenCalledWith(
expect.objectContaining({
workItemId: 102,
chatJid,
deliveryRole: 'reviewer',
taskId: 'task-fallback-review-delivery-role',
pendingTaskStatus: 'merge_ready',
intentKind: 'finalize-owner-turn',
}),
'Skipping queued follow-up after reviewer merge_ready delivery because inline finalize will handle the handoff',
'Queued paired follow-up after delivery retry',
);
});
@@ -1920,6 +1924,287 @@ If your first line is DONE_WITH_CONCERNS, the system will reopen review instead
);
});
it('re-enqueues owner after a successful reviewer delivery moves the task back to active', async () => {
const chatJid = 'group@test';
const group = makeGroup('codex');
const ownerChannel = makeChannel(chatJid);
const reviewerChannel = makeChannel(chatJid, 'discord-review', false);
const enqueueMessageCheck = vi.fn();
const pairedTask = {
id: 'task-reviewer-delivery-owner-follow-up',
chat_jid: chatJid,
group_folder: group.folder,
owner_service_id: 'claude',
reviewer_service_id: 'codex-main',
title: null,
source_ref: 'HEAD',
plan_notes: null,
review_requested_at: '2026-03-30T00:00:00.000Z',
round_trip_count: 1,
status: 'review_ready',
arbiter_verdict: null,
arbiter_requested_at: null,
completion_reason: null,
created_at: '2026-03-30T00:00:00.000Z',
updated_at: '2026-03-30T00:00:00.000Z',
} as any;
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
vi.mocked(db.getLatestOpenPairedTaskForChat).mockImplementation(
() => pairedTask,
);
vi.mocked(agentRunner.runAgentProcess).mockImplementation(
async (_group, _input, _onProcess, onOutput) => {
pairedTask.status = 'active';
await onOutput?.({
status: 'success',
phase: 'final',
result: 'DONE_WITH_CONCERNS\nreviewer follow-up needed',
newSessionId: 'session-reviewer-delivery-owner-follow-up',
});
return {
status: 'success',
result: 'DONE_WITH_CONCERNS\nreviewer follow-up needed',
newSessionId: 'session-reviewer-delivery-owner-follow-up',
};
},
);
const runtime = createMessageRuntime({
assistantName: 'Andy',
idleTimeout: 1_000,
pollInterval: 1_000,
timezone: 'UTC',
triggerPattern: /^@Andy\b/i,
channels: [ownerChannel, reviewerChannel],
queue: {
registerProcess: vi.fn(),
closeStdin: vi.fn(),
notifyIdle: vi.fn(),
enqueueMessageCheck,
} as any,
getRegisteredGroups: () => ({ [chatJid]: group }),
getSessions: () => ({}),
getLastTimestamp: () => '',
setLastTimestamp: vi.fn(),
getLastAgentTimestamps: () => ({}),
saveState: vi.fn(),
persistSession: vi.fn(),
clearSession: vi.fn(),
});
const result = await runtime.processGroupMessages(chatJid, {
runId: 'run-reviewer-delivery-owner-follow-up',
reason: 'messages',
});
expect(result).toBe(true);
expect(reviewerChannel.sendMessage).toHaveBeenCalledWith(
chatJid,
'DONE_WITH_CONCERNS\nreviewer follow-up needed',
);
expect(ownerChannel.sendMessage).not.toHaveBeenCalled();
expect(enqueueMessageCheck).toHaveBeenCalledWith(chatJid);
expect(logger.info).toHaveBeenCalledWith(
expect.objectContaining({
chatJid,
runId: 'run-reviewer-delivery-owner-follow-up',
completedRole: 'reviewer',
taskId: 'task-reviewer-delivery-owner-follow-up',
taskStatus: 'active',
}),
'Queued paired follow-up after successful reviewer/arbiter delivery',
);
});
it('re-enqueues finalize-owner after a successful reviewer delivery moves the task to merge_ready', async () => {
const chatJid = 'group@test';
const group = makeGroup('codex');
const ownerChannel = makeChannel(chatJid);
const reviewerChannel = makeChannel(chatJid, 'discord-review', false);
const enqueueMessageCheck = vi.fn();
const pairedTask = {
id: 'task-reviewer-delivery-finalize-owner',
chat_jid: chatJid,
group_folder: group.folder,
owner_service_id: 'claude',
reviewer_service_id: 'codex-main',
title: null,
source_ref: 'HEAD',
plan_notes: null,
review_requested_at: '2026-03-30T00:00:00.000Z',
round_trip_count: 1,
status: 'review_ready',
arbiter_verdict: null,
arbiter_requested_at: null,
completion_reason: null,
created_at: '2026-03-30T00:00:00.000Z',
updated_at: '2026-03-30T00:00:00.000Z',
} as any;
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
vi.mocked(db.getLatestOpenPairedTaskForChat).mockImplementation(
() => pairedTask,
);
vi.mocked(agentRunner.runAgentProcess).mockImplementation(
async (_group, _input, _onProcess, onOutput) => {
pairedTask.status = 'merge_ready';
await onOutput?.({
status: 'success',
phase: 'final',
result: 'DONE\nreview approved',
newSessionId: 'session-reviewer-delivery-finalize-owner',
});
return {
status: 'success',
result: 'DONE\nreview approved',
newSessionId: 'session-reviewer-delivery-finalize-owner',
};
},
);
const runtime = createMessageRuntime({
assistantName: 'Andy',
idleTimeout: 1_000,
pollInterval: 1_000,
timezone: 'UTC',
triggerPattern: /^@Andy\b/i,
channels: [ownerChannel, reviewerChannel],
queue: {
registerProcess: vi.fn(),
closeStdin: vi.fn(),
notifyIdle: vi.fn(),
enqueueMessageCheck,
} as any,
getRegisteredGroups: () => ({ [chatJid]: group }),
getSessions: () => ({}),
getLastTimestamp: () => '',
setLastTimestamp: vi.fn(),
getLastAgentTimestamps: () => ({}),
saveState: vi.fn(),
persistSession: vi.fn(),
clearSession: vi.fn(),
});
const result = await runtime.processGroupMessages(chatJid, {
runId: 'run-reviewer-delivery-finalize-owner',
reason: 'messages',
});
expect(result).toBe(true);
expect(reviewerChannel.sendMessage).toHaveBeenCalledWith(
chatJid,
'DONE\nreview approved',
);
expect(ownerChannel.sendMessage).not.toHaveBeenCalled();
expect(enqueueMessageCheck).toHaveBeenCalledWith(chatJid);
expect(logger.info).toHaveBeenCalledWith(
expect.objectContaining({
chatJid,
runId: 'run-reviewer-delivery-finalize-owner',
completedRole: 'reviewer',
taskId: 'task-reviewer-delivery-finalize-owner',
taskStatus: 'merge_ready',
}),
'Queued paired follow-up after successful reviewer/arbiter delivery',
);
});
it('re-enqueues owner after a successful arbiter delivery moves the task back to active', async () => {
const chatJid = 'group@test';
const group = makeGroup('codex');
const ownerChannel = makeChannel(chatJid);
const reviewerChannel = makeChannel(chatJid, 'discord-review', false);
const arbiterChannel = makeChannel(chatJid, 'discord-arbiter', false);
const enqueueMessageCheck = vi.fn();
const pairedTask = {
id: 'task-arbiter-delivery-owner-follow-up',
chat_jid: chatJid,
group_folder: group.folder,
owner_service_id: 'claude',
reviewer_service_id: 'codex-main',
arbiter_service_id: 'claude-arbiter',
title: null,
source_ref: 'HEAD',
plan_notes: null,
review_requested_at: '2026-03-30T00:00:00.000Z',
round_trip_count: 1,
status: 'arbiter_requested',
arbiter_verdict: null,
arbiter_requested_at: '2026-03-30T00:00:10.000Z',
completion_reason: null,
created_at: '2026-03-30T00:00:00.000Z',
updated_at: '2026-03-30T00:00:00.000Z',
} as any;
vi.mocked(serviceRouting.hasReviewerLease).mockReturnValue(true);
vi.mocked(db.getLatestOpenPairedTaskForChat).mockImplementation(
() => pairedTask,
);
vi.mocked(agentRunner.runAgentProcess).mockImplementation(
async (_group, _input, _onProcess, onOutput) => {
pairedTask.status = 'active';
await onOutput?.({
status: 'success',
phase: 'final',
result: 'DONE_WITH_CONCERNS\narbiter says revise',
newSessionId: 'session-arbiter-delivery-owner-follow-up',
});
return {
status: 'success',
result: 'DONE_WITH_CONCERNS\narbiter says revise',
newSessionId: 'session-arbiter-delivery-owner-follow-up',
};
},
);
const runtime = createMessageRuntime({
assistantName: 'Andy',
idleTimeout: 1_000,
pollInterval: 1_000,
timezone: 'UTC',
triggerPattern: /^@Andy\b/i,
channels: [ownerChannel, reviewerChannel, arbiterChannel],
queue: {
registerProcess: vi.fn(),
closeStdin: vi.fn(),
notifyIdle: vi.fn(),
enqueueMessageCheck,
} as any,
getRegisteredGroups: () => ({ [chatJid]: group }),
getSessions: () => ({}),
getLastTimestamp: () => '',
setLastTimestamp: vi.fn(),
getLastAgentTimestamps: () => ({}),
saveState: vi.fn(),
persistSession: vi.fn(),
clearSession: vi.fn(),
});
const result = await runtime.processGroupMessages(chatJid, {
runId: 'run-arbiter-delivery-owner-follow-up',
reason: 'messages',
});
expect(result).toBe(true);
expect(arbiterChannel.sendMessage).toHaveBeenCalledWith(
chatJid,
'DONE_WITH_CONCERNS\narbiter says revise',
);
expect(ownerChannel.sendMessage).not.toHaveBeenCalled();
expect(enqueueMessageCheck).toHaveBeenCalledWith(chatJid);
expect(logger.info).toHaveBeenCalledWith(
expect.objectContaining({
chatJid,
runId: 'run-arbiter-delivery-owner-follow-up',
completedRole: 'arbiter',
taskId: 'task-arbiter-delivery-owner-follow-up',
taskStatus: 'active',
}),
'Queued paired follow-up after successful reviewer/arbiter delivery',
);
});
it('does not enqueue the same reviewer follow-up twice across different runs while task state is unchanged', async () => {
const chatJid = 'group@test';
const group = makeGroup('codex');

View File

@@ -343,16 +343,17 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
const { deliverySucceeded, visiblePhase } =
await turnController.finish(outputStatus);
if (deliverySucceeded && pairedRoom && resolvedDeliveryRole === 'owner') {
if (deliverySucceeded && pairedRoom && resolvedDeliveryRole) {
const pendingTaskAfterDelivery =
getLatestOpenPairedTaskForChat(chatJid);
const nextTurnAction = resolveNextTurnAction({
taskStatus: pendingTaskAfterDelivery?.status,
lastTurnOutputRole: 'owner',
lastTurnOutputRole: resolvedDeliveryRole,
});
const dispatch = resolveFollowUpDispatch({
source: 'owner-delivery-success',
source: 'delivery-success',
nextTurnAction,
completedRole: resolvedDeliveryRole,
});
if (
dispatch.kind === 'enqueue' &&
@@ -376,8 +377,12 @@ export function createMessageRuntime(deps: MessageRuntimeDeps): {
scheduled,
},
scheduled
? 'Queued paired follow-up after successful owner delivery'
: 'Skipped duplicate paired follow-up after successful owner delivery while task state was unchanged',
? resolvedDeliveryRole === 'owner'
? 'Queued paired follow-up after successful owner delivery'
: 'Queued paired follow-up after successful reviewer/arbiter delivery'
: resolvedDeliveryRole === 'owner'
? 'Skipped duplicate paired follow-up after successful owner delivery while task state was unchanged'
: 'Skipped duplicate paired follow-up after successful reviewer/arbiter delivery while task state was unchanged',
);
}
}