Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1039,15 +1039,15 @@ jobs:
npm install --global --prefix "$test_root/install" \
--include=optional --ignore-scripts --no-audit --no-fund \
--registry=https://registry.npmjs.org/ \
"@xgen/cli@$version"; then
"@xgen-ai/cli@$version"; then
break
fi
test "$attempt" -lt 3
sleep 5
done
global_root=$(npm root --global --prefix "$test_root/install")
test -f "$global_root/@xgen/cli/package.json"
test -f "$global_root/@xgen/$PLATFORM_PACKAGE/package.json"
test -f "$global_root/@xgen-ai/cli/package.json"
test -f "$global_root/@xgen-ai/$PLATFORM_PACKAGE/package.json"
HOME="$test_root/home" \
XGEN_STATE_HOME="$test_root/state" \
"$test_root/install/bin/xgen" --version > "$test_root/version.txt"
Expand All @@ -1072,19 +1072,19 @@ jobs:
npm install --global --prefix "$test_root/install" \
--include=optional --ignore-scripts --no-audit --no-fund \
--registry=https://registry.npmjs.org/ \
"@xgen/cli@$version"
test -f "$global_root/@xgen/cli/package.json"
test -f "$global_root/@xgen/$PLATFORM_PACKAGE/package.json"
"@xgen-ai/cli@$version"
test -f "$global_root/@xgen-ai/cli/package.json"
test -f "$global_root/@xgen-ai/$PLATFORM_PACKAGE/package.json"
test "$(HOME="$test_root/home" XGEN_STATE_HOME="$test_root/state" \
"$test_root/install/bin/xgen" --version)" = "xgen $version"
test ! -e "$test_root/state"

HOME="$test_root/home" \
npm_config_cache="$test_root/cache" \
npm uninstall --global --prefix "$test_root/install" \
--ignore-scripts --no-audit --no-fund '@xgen/cli'
test ! -e "$global_root/@xgen/cli"
test ! -e "$global_root/@xgen/$PLATFORM_PACKAGE"
--ignore-scripts --no-audit --no-fund '@xgen-ai/cli'
test ! -e "$global_root/@xgen-ai/cli"
test ! -e "$global_root/@xgen-ai/$PLATFORM_PACKAGE"
test ! -e "$test_root/install/bin/xgen"

- name: Install and verify exact npm package on Windows
Expand Down Expand Up @@ -1114,7 +1114,7 @@ jobs:
& npm.cmd install --global --prefix $InstallRoot `
--include=optional --ignore-scripts --no-audit --no-fund `
--registry=https://registry.npmjs.org/ `
"@xgen/cli@$Version"
"@xgen-ai/cli@$Version"
if ($LASTEXITCODE -eq 0) {
$Installed = $true
break
Expand All @@ -1123,10 +1123,10 @@ jobs:
}
if (-not $Installed) { throw "npm install failed" }
$GlobalRoot = (& npm.cmd root --global --prefix $InstallRoot | Out-String).Trim()
if (-not (Test-Path -LiteralPath (Join-Path $GlobalRoot "@xgen/cli/package.json"))) {
if (-not (Test-Path -LiteralPath (Join-Path $GlobalRoot "@xgen-ai/cli/package.json"))) {
throw "launcher package is missing"
}
if (-not (Test-Path -LiteralPath (Join-Path $GlobalRoot "@xgen/$env:PLATFORM_PACKAGE/package.json"))) {
if (-not (Test-Path -LiteralPath (Join-Path $GlobalRoot "@xgen-ai/$env:PLATFORM_PACKAGE/package.json"))) {
throw "platform package is missing"
}
$Shim = Join-Path $InstallRoot "xgen.cmd"
Expand Down Expand Up @@ -1158,15 +1158,15 @@ jobs:
& npm.cmd install --global --prefix $InstallRoot `
--include=optional --ignore-scripts --no-audit --no-fund `
--registry=https://registry.npmjs.org/ `
"@xgen/cli@$Version"
"@xgen-ai/cli@$Version"
if ($LASTEXITCODE -ne 0) { throw "npm reinstall failed" }
$Reinstalled = (& $Shim --version | Out-String).Trim()
if ($LASTEXITCODE -ne 0 -or $Reinstalled -ne "xgen $Version") {
throw "npm reinstall produced the wrong version"
}
if (
-not (Test-Path -LiteralPath (Join-Path $GlobalRoot "@xgen/cli/package.json")) -or
-not (Test-Path -LiteralPath (Join-Path $GlobalRoot "@xgen/$env:PLATFORM_PACKAGE/package.json"))
-not (Test-Path -LiteralPath (Join-Path $GlobalRoot "@xgen-ai/cli/package.json")) -or
-not (Test-Path -LiteralPath (Join-Path $GlobalRoot "@xgen-ai/$env:PLATFORM_PACKAGE/package.json"))
) {
throw "npm reinstall is missing an exact package"
}
Expand All @@ -1175,11 +1175,11 @@ jobs:
}

& npm.cmd uninstall --global --prefix $InstallRoot `
--ignore-scripts --no-audit --no-fund "@xgen/cli"
--ignore-scripts --no-audit --no-fund "@xgen-ai/cli"
if ($LASTEXITCODE -ne 0) { throw "npm uninstall failed" }
if (
(Test-Path -LiteralPath (Join-Path $GlobalRoot "@xgen/cli")) -or
(Test-Path -LiteralPath (Join-Path $GlobalRoot "@xgen/$env:PLATFORM_PACKAGE"))
(Test-Path -LiteralPath (Join-Path $GlobalRoot "@xgen-ai/cli")) -or
(Test-Path -LiteralPath (Join-Path $GlobalRoot "@xgen-ai/$env:PLATFORM_PACKAGE"))
) {
throw "npm uninstall left package artifacts"
}
Expand Down
6 changes: 5 additions & 1 deletion MEMORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# xgen CLI와 연계 도구 공동 개발 메모

갱신: 2026-10-06 (KST)
갱신: 2026-10-24 (KST)

## 사용자가 정한 방향

Expand Down Expand Up @@ -137,4 +137,8 @@
- 명세 없는 API 복원 PoC(2026-10-09, 프론트 번들 정적 분석): jsluice는 템플릿 URL(`/api/lists/${id}`)을 놓쳐 acorn AST 추출기를 따로 만들었다. 정밀도는 높지만(4앱 89~100%) 복원율은 프론트 호출 방식에 좌우된다(fetch wrapper 함수를 쓰는 Traccar 16/117). 압축 후에도 클래스 메서드 이름은 살아 있어 요약으로 쓰는 게 LLM 설명보다 낫고, 이름이 사라진 경우만 LLM이 돕는다. 공식 명세가 낡아 번들이 더 최신 API를 보여주는 경우가 있다(listmonk 23개). 검증셋(Kavita·Traccar)은 소진. 평가: ~/.local/share/xgen/evaluations/bundle-api-2026-10-09/PLAN.md
- 명세 없는 API 복원 PoC 2(2026-10-10, Spring jar 바이트코드): 복원은 거의 완전(recall 99.7~100%, 정답 밖 초과분 대부분 springdoc @Hidden 실제 라우트)인데 검색은 원래 명세에 짐. 원인은 한국어 질의 대 영어 식별자(한국어 12건 중 1~3건만 hit, 영어는 같거나 우세). 복원 품질과 검색 품질은 별개라 언어 맞춤 설명 단계가 필요. 큰 초안은 DTO inline 시 로컬 소스 5MB 상한 초과 -> components $ref. 평가: ~/.local/share/xgen/evaluations/jar-api-2026-10-10/PLAN.md
- 언어 병목 비교(2026-10-13): 검색어 힌트(Q)는 새 공개 held-out에서 hit@5 76 -> 80으로 유지했다(hit@1은 43 -> 37). 수집 시점 summary 보강(S)은 hit@5 87, hit@1 66으로 가장 좋았지만, QS 대 Q 판정에서 한 칸이 3 하락해 불채택했다. 보강 효과 대부분은 같은 언어 칸의 summary 보강에서 나왔고, 한국어 요청 x 영어 명세와 summary 없는 명세는 오르지 않았다. graph-tool-call 0.47 색인은 summary(없으면 METHOD /path)만 쓰고 description은 쓰지 않는다. 다음 후보는 S 단독 대 Q 직접 판정이다. 평가: ~/.local/share/xgen/evaluations/language-sides-2026-10-13/PLAN.md
- npm scope `@xgen-ai`와 bootstrap 게시(2026-10-24, 게시 완료·레지스트리 확인): ADR-0031이 정한 `@xgen` scope는 **다른 계정이 이미 선점**하고 있었다. 그 아래 게시된 package는 0개지만 npm은 user와 org가 같은 namespace라 org로도 못 만든다. 판별은 `registry.npmjs.org/-/org/<이름>/user`가 200이면 선점, 404 `Scope not found`면 빈 이름이다. 레포 runbook이 "scope 확보가 불명확하면 게시하지 말고 naming ADR을 먼저 갱신"으로 fail-closed를 걸어 둬서 추정으로 진행할 수 없었다. 실측 가용: 빈 이름 `@xgen-ai`·`@xgen2bee`·`@xgencli`, 선점 `@xgen`·`@x2bee`·`@plateer`·`@plateerlab`·`@xgenai`. `@xgen-ai`를 골랐다(회사 공개 배포 식별자가 이미 그 이름). 개인 계정 scope는 소유권 이전 시 게시된 6개를 deprecate해야 해서 뺐다. ADR-0051을 쓰고 ADR-0031·0034·0046 상태 줄이 그것을 가리키게 했다(본문은 당시 기록이라 `@xgen` 유지, 0031이 0034에 한 방식 그대로). rename 함정: `@xgen/` 문자열 치환이 **네 곳을 지나친다** — `npm/scripts/smoke.mjs`의 `path.join(globalRoot, '@xgen', 'cli')`, `npm/scripts/check.mjs`의 이스케이프된 정규식 `/^@xgen\/cli-.../`, 문서 두 곳의 slash 없는 scope 언급. 앞 둘은 동작 경로라 놓치면 smoke install과 contract check가 옛 이름을 찾는다. bootstrap 6개는 `0.0.0-bootstrap.0`으로 platform 5개 먼저·launcher 마지막 순서로 `--access public --tag bootstrap` 게시했고, 내용은 `package.json`·`README.md`·`LICENSE`뿐이며 `bin`도 `scripts`도 없다(설치 시 실행되는 코드 없음). 게시 직후 registry API는 몇 분간 404다 — "being processed"는 정상이고 전파를 기다려야 한다. GitHub은 secret `NPM_TOKEN` 갱신 + variable `XGEN_NPM_PUBLISH_ENABLED=true`까지 했다. repo variable에 `XGENY_*` 4개가 개명 잔재로 남아 있고 release workflow는 `XGEN_*`만 읽는다(미정리).
- `run`·`resume`의 진행 표시(2026-10-24, 구현 완료·로컬 검증): `--debug`는 전역 플래그이고 help에 "durable progress events를 보여준다"고 적혀 있지만 `run`·`resume`에서는 **파싱만 되고 버려졌다**(`cli.debug`의 유일한 사용처가 `interactive_command`). 조용히 아무 일도 안 하는 플래그였다. `run_local_with_progress`와 `resume_local_with_model_resolver_and_progress`는 이미 public이었고 `run`만 `run_local_with_started`를 고르고 있었으므로, 호출만 바꾸고 REPL의 `print_progress` 하나를 `pub(crate)`로 올려 같은 formatter를 공유했다. 설계 선택 둘. (1) spinner가 아니라 event 줄을 쓴다 — `run`의 stderr는 이미 `XGEN_*` 줄 계약이라 줄 단위가 pipe·로그와 맞고, spinner는 `XGEN_STARTED`와 커서가 겹쳐 `\r\x1b[2K` 조율이 필요하다(spinner는 프롬프트로 돌아갈 자리가 있는 REPL 것). (2) 기본값은 침묵 — 결과 줄만 파싱하는 host에 묻지 않은 `progress:` prefix를 주지 않는다. 쓰기 실패는 보고만 멈추고 Run은 계속한다(진단 한 줄 때문에 durable 작업을 버리지 않음, `run`의 중단은 process-level signal handler 소유). `resume`은 egress 유무로 갈렸던 두 경로를 `..._and_progress` 하나로 합쳤다(no-egress는 거절하는 resolver, 기존 `resume_local`과 동일). 검증: `--debug`에서 `progress: model_call_starting`·`response_committed`, 없으면 0건, stdout은 양쪽 모두 최종 응답 바이트만(테스트가 `assert_eq`로 고정). 플래그를 안 넘기도록 되돌려 테스트 실패 확인. 전체 691 passed·0 failed(4 ignored), clippy·fmt·공개 문서 검사 통과.
- `run`의 실행 파일 catalog(2026-10-24, 구현 완료·로컬 검증): `--allow-execute`만 주면 `configuration_mismatch`로 죽는데, 이건 버그가 아니라 **의도된 계약**이고 고정 테스트도 있다(protocol_check.rs). 이유는 매니페스트에서 확인했다. execution profile digest에 `environment:PATH`와 catalog된 실행 파일 각각의 해시가 들어가므로, PATH 자동 발견을 기본값으로 하면 같은 명령이 머신마다 다른 프로필을 만들고 `resume`이 프로필 불일치로 닫힌다. REPL은 한 프로세스·한 세션이라 자동 발견해도 되고 `run`은 아니다. 그래서 기본값을 뒤집지 않고 `--allow-path-executables`를 opt-in으로 더했다(`run`·`resume` 양쪽, resume에도 같은 플래그 필요). `--allow-executable` 항목은 그대로 유지되고 이름 충돌 시 이긴다(실측: 명시한 `git=/nonexistent/git`은 거절, 빼면 발견된 git으로 통과). 자동 발견은 **shell을 넣지 않는다** — ADR-0049가 "catalogued executable을 shell 없이" 시작하도록 정했고 PTY session은 Linux 전용이다. `sh -c "<명령>"`은 실제 명령을 argument에 숨겨 Receipt를 무의미하게 만든다. 따라서 Pocket Agent 쪽 shim의 `with_shell`(`--allow-executable sh=<path>`)은 이 플래그로 대체되지 않는다 — shell이 필요하면 host가 이름으로 직접 넣는 것이 설계상 맞는 경로다. 빈 catalog 실패는 어느 쪽이 빠졌는지와 두 가지 해결책을 함께 출력하게 했다(`PublicRunError::Configuration`은 54곳 공용이라 코드만으로는 아무 정보가 없다). 검증: loopback fake provider(probe=completion_candidate / 대화 턴=response_candidate 구분 필요, 안 하면 `completion_without_receipt_completed_plan`으로 보여 제품 결함처럼 읽힌다)로 `run`이 발견된 catalog + `--conversation-responses`로 exit 0 응답까지 확인. egress 동의 검사가 catalog 검사보다 **뒤**에 있어서, 동의 없이 돌리면 pause 지점이 catalog 수용 여부를 네트워크 없이 그대로 드러낸다 — 테스트에 이 성질을 썼다. 전체 690 passed·0 failed(4 ignored), clippy·fmt·공개 문서 검사 통과.
- provider preset과 base URL 규칙(2026-10-24, 구현 완료·로컬 검증): 터미널에서 opencode처럼 쓰려면 provider 선택이 먼저라 preset을 2개에서 5개로 늘렸다(deepseek·openai·openrouter·glm·local, 그 밖은 custom). 함정 셋. (1) `api_endpoints`가 base URL의 path가 `/v1`이길 **강제**해서 자기 API를 다르게 번호 붙이는 provider는 설정 자체가 불가였다(z.ai `/api/paas/v4`, Azure OpenAI `/openai/deployments/<name>`). `/v1`은 관례일 뿐이라 빈 path만 거절하도록 바꿨고 오류 코드는 `base_url_must_end_in_v1` -> `base_url_must_name_the_api_root`다. (2) preset은 사용자가 고르지 않은 값으로 실패할 수 있어 preset 자체를 검사해야 한다. `local`을 `http://localhost:11434/v1`로 썼더니 평문 HTTP는 loopback **literal**만 허용해서 네트워크에 닿기도 전에 `plaintext_endpoint_must_be_loopback`으로 죽었다. `127.0.0.1`로 고치고, 모든 preset이 설정 규칙(base path + transport security)을 통과하는지 보는 테스트를 넣었다(되돌려 실패 확인). (3) Claude는 preset에 넣지 않는다. OpenAI 호환 layer가 `response_format`을 무시한다고 문서에 적혀 있어서 `model setup`은 PASS하고 모든 Run이 깨진다. 설정은 성공하고 실행만 실패하는 preset은 편의가 아니라 지뢰다. OpenRouter 같은 gateway 경유는 된다. GLM은 pay-as-you-go와 Coding Plan의 경로가 달라(`/api/coding/paas/v4`) 주석·문서에 적었다. 검증: loopback fake provider로 picker 6항목·`local` preset·비-`/v1` 루트(`/api/paas/v4`)·trailing slash 정규화·bare host 거절 4종과 `model list|use|check`를 실측했다. preset의 `response_format` 값 자체는 DeepSeek·local만 실측이고 나머지는 provider 문서 기준으로 문서에 구분 표시했다. 로컬 전체 검사 688 passed·0 failed(4 ignored), clippy·fmt·공개 문서 검사 통과. 맥에서는 `scripts/tests` 중 `test_timeout_stops_owned_process_descendants` 하나가 `os.pidfd_open`(Linux 전용) 때문에 원래 실패한다 — eval 스크립트의 이식성 문제이고 제품 경로가 아니다.
- collection 운영 명령(2026-10-13, #88): `tools rebuild|disconnect|remove`와 `list` 상태를 추가했다. 함정 두 가지. graph-tool-call artifact에 빌드 시각(`built_at`, `created_at`)이 들어가 같은 입력도 다시 빌드하면 digest가 바뀐다. 그래서 rebuild는 backend 버전이 다를 때만 교체한다. 연결은 collection 이름으로 붙으므로, collection 파일만 지우면 같은 이름의 다른 API가 이전 주소와 토큰을 물려받는다. 그래서 remove는 연결부터 지우고, import는 연결만 남은 이름을 거절한다. CI의 Python 단계는 graph-tool-call 없이 돌아 worker·CLI 전 과정 테스트가 skip되므로 로컬에서 확인한다.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Node.js가 이미 있다면 exact RC를 npm으로 설치할 수 있습니다. `-
제거하므로 사용하지 않습니다.

```bash
npm install --global @xgen/cli@0.1.0-rc.3
npm install --global @xgen-ai/cli@0.1.0-rc.3
xgen --version
```

Expand Down
2 changes: 1 addition & 1 deletion crates/xgen-cli/src/composition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ impl ModelCheckError {
match self {
Self::Configuration => "configuration_invalid",
Self::InvalidBaseUrl => "base_url_invalid",
Self::InvalidBasePath => "base_url_must_end_in_v1",
Self::InvalidBasePath => "base_url_must_name_the_api_root",
Self::InvalidModel => "model_invalid",
Self::InvalidTokenizer => "tokenizer_invalid",
Self::InvalidCredential => "api_key_invalid",
Expand Down
Loading