Skip to content
Draft
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ google-jules-skill/
python3 google-jules-control/scripts/jules_api.py doctor --compact
```

키가 실제 API 인증까지 통과하는지 확인하려면 아래처럼 검증 probe를 추가합니다.
Add the validation probe when you need to confirm that the key authenticates with the API.

```bash
python3 google-jules-control/scripts/jules_api.py doctor --compact --validate-api
```

4. 저장소를 Jules source로 해석합니다.
Resolve a repository to a Jules source.

Expand Down
16 changes: 10 additions & 6 deletions docs/setup-and-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,23 @@ The script checks the current working directory first, then the skill root.

```bash
python3 google-jules-control/scripts/jules_api.py doctor --compact
python3 google-jules-control/scripts/jules_api.py doctor --compact --validate-api
```

정상 예시 / Healthy example:
검증 포함 정상 예시 / Healthy example with API validation:

```text
dotenv=yes api_key=yes api_ready=yes gh=yes gh_auth=yes merge_ready=yes jules_cli=no jules_cli_auth=not_installed cli_ready=no ready=yes
dotenv=yes api_key=yes api_validated=yes api_status=ok api_ready=yes gh=yes gh_auth=yes merge_ready=yes jules_cli=no jules_cli_auth=not_installed cli_ready=no ready=yes
```

REST API만 쓴다면 `jules_cli=no`는 문제 아닙니다.
`jules_cli=no` is acceptable if you only use the REST API path.

`api_ready=yes`는 API helper가 바로 실행 가능한 상태를 뜻합니다.
`api_ready=yes` means the REST API helper can run.
기본 `doctor --compact`는 네트워크 API 호출을 하지 않습니다. 이 경우 `api_key=yes`는 키가 있다는 뜻이고, `api_ready=yes`는 아닙니다.
Plain `doctor --compact` does not make a network API call. In that mode, `api_key=yes` means the key is present, not that `api_ready=yes`.

`api_ready=yes`는 `--validate-api` probe가 성공해서 현재 키로 Jules API 인증이 확인됐다는 뜻입니다.
`api_ready=yes` means the `--validate-api` probe succeeded and the current key authenticated with the Jules API.

`cli_ready=yes`는 Jules CLI 경로도 바로 쓸 수 있다는 뜻입니다.
`cli_ready=yes` means the Jules CLI path is also ready to use.
Expand Down Expand Up @@ -127,8 +131,8 @@ python3 google-jules-control/scripts/jules_api.py stale-session-report --repo-fi

- `ready=no`: `doctor`를 `--compact` 없이 실행해서 어떤 항목이 비었는지 확인합니다.
Run `doctor` without `--compact` to see the missing dependency.
- `api_ready=no`: `.env`와 `JULES_API_KEY`를 먼저 점검합니다.
Check `.env` and `JULES_API_KEY` first.
- `api_ready=no`: `api_key`, `api_validated`, `api_status`를 함께 봅니다. 검증이 필요하면 `doctor --compact --validate-api`를 실행합니다.
Check `api_key`, `api_validated`, and `api_status` together. Run `doctor --compact --validate-api` when you need credential validation.
- `cli_ready=no`: CLI 경로를 쓰려면 `jules login` 또는 CLI 인증 상태를 확인합니다.
If you want the CLI path, check `jules login` or the CLI authentication state.
- `merge_ready=no`: merge 기반 리포트 전에 `gh auth status`를 확인합니다.
Expand Down
6 changes: 4 additions & 2 deletions google-jules-control/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Use this skill to delegate coding work to Google Jules from an agentic workflow.
- API path: put `JULES_API_KEY` in a `.env` file from `https://jules.google.com/settings`.
- CLI path: install `@google/jules`, then run `jules login`.
- API/CLI health check: run `python3 scripts/jules_api.py doctor --compact`.
- API credential validation: run `python3 scripts/jules_api.py doctor --compact --validate-api`.
- Merge-aware reporting health check: run `python3 scripts/jules_api.py gh-auth-check --compact`.
2. Discover the target repository/source.
- API path: run `python3 scripts/jules_api.py repo-to-source --repo owner/repo --compact` or `python3 scripts/jules_api.py list-sources`.
Expand Down Expand Up @@ -115,7 +116,8 @@ python3 scripts/jules_api.py summary --session sessions/1234567890

Notes:

- `doctor --compact` now separates `api_ready`, `cli_ready`, and `merge_ready`.
- `doctor --compact` separates `api_key`, `api_validated`, `api_ready`, `cli_ready`, and `merge_ready`.
- `api_key=yes` only means the key is present. Use `doctor --compact --validate-api` when you need to prove that the key can authenticate.
- Aggregated list/report commands collect all pages by default. `--page-token` is a starting point for aggregation, not a single-page mode.

### CLI workflow
Expand Down Expand Up @@ -474,7 +476,7 @@ python3 scripts/jules_api.py close-ready-report --repo-filter owner/repo --requi
- Use `close-ready-report` when the user wants close candidates plus ready-made close confirmation text in one step.
- Prefer `--markdown` for human review and `--compact` for scripting or automation chaining.
- Prefer a local `.env` file over shell profile edits for `JULES_API_KEY`. The script auto-loads `.env` from the current working directory or the skill root.
- Use `doctor` before the first live run to verify `.env`, `JULES_API_KEY`, `gh`, and `jules` CLI readiness in one place.
- Use `doctor` before the first live run to inspect `.env`, `JULES_API_KEY`, `gh`, and `jules` CLI readiness in one place. Add `--validate-api` when API credential validity matters.
- Use `repo-to-source` when the user gives only `owner/repo` and you need the exact Jules source resource name.
- Use `check-pr-readiness` before cleanup when you need to know whether a PR is actually merge-ready, not just merged or open.
- Use `request-pr-rework` when GitHub reports blockers such as merge conflicts, failed checks, or requested changes and you want a follow-up message for Jules.
Expand Down
3 changes: 2 additions & 1 deletion google-jules-control/references/jules-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ The Jules REST API does not expose GitHub merge state directly. To close a sessi
The bundled `jules_api.py` script automates this with:

- `doctor --compact`
- `doctor --compact --validate-api`
- `gh-auth-check --compact`
- `repo-to-source --repo owner/repo --compact`
- `cleanup-report --repo-filter owner/repo --require-all-merged`
Expand All @@ -115,7 +116,7 @@ Implementation detail:
- Merge readiness is checked through `gh pr view <url> --json mergeable,mergeStateStatus,reviewDecision,statusCheckRollup,...`.
- If `gh` is missing or unauthenticated, merge status falls back to `unknown` and close should not proceed automatically.
- `gh-auth-check` is the fast preflight check before any merge-aware report.
- `doctor` separates `api_ready`, `cli_ready`, and `merge_ready`. `ready=yes` means at least one control path is available.
- `doctor` separates `api_key`, `api_validated`, `api_ready`, `cli_ready`, and `merge_ready`. Without `--validate-api`, `api_key=yes` only means the key is present. With `--validate-api`, `api_ready=yes` means the API probe authenticated successfully. `ready=yes` means at least one validated control path is available.
- `close-ready-report` distinguishes between `candidates` and `cautionCandidates`. Treat caution entries as manual-review items, not automatic close targets.
- `close-merged-session` refuses `caution` sessions by default. Only use `--allow-caution-close` after explicit user approval.

Expand Down
50 changes: 49 additions & 1 deletion google-jules-control/scripts/jules_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from __future__ import annotations

import argparse
import contextlib
import datetime as dt
import io
import json
import os
from pathlib import Path
Expand Down Expand Up @@ -422,6 +424,43 @@ def collect_jules_cli_status() -> dict[str, Any]:
return payload


def collect_api_validation_status(*, validate: bool) -> dict[str, Any]:
api_key_present = bool(os.environ.get("JULES_API_KEY", "").strip())
if not validate:
return {
"validated": False,
"ready": False,
"status": "not_checked",
"reason": "Run doctor --validate-api to verify Jules API credentials.",
}
if not api_key_present:
return {
"validated": True,
"ready": False,
"status": "missing_key",
"reason": "JULES_API_KEY is not set.",
}

stderr = io.StringIO()
try:
with contextlib.redirect_stderr(stderr):
api_request("GET", "/sources", query={"pageSize": 1})
except SystemExit as exc:
reason = stderr.getvalue().strip() or f"API validation failed with exit code {exc.code}."
return {
"validated": True,
"ready": False,
"status": "failed",
"reason": reason,
}

return {
"validated": True,
"ready": True,
"status": "ok",
}


def format_session_line(session: dict[str, Any]) -> str:
repo = session.get("repo") or "-"
title = session.get("title") or "(untitled)"
Expand Down Expand Up @@ -1627,7 +1666,8 @@ def doctor(args: argparse.Namespace) -> None:
api_key_present = bool(os.environ.get("JULES_API_KEY", "").strip())
gh_status = collect_gh_auth_status()
jules_status = collect_jules_cli_status()
api_ready = api_key_present
api_validation = collect_api_validation_status(validate=args.validate_api)
api_ready = bool(api_validation["ready"])
cli_ready = bool(jules_status.get("ready"))
merge_ready = bool(gh_status.get("installed") and gh_status.get("authenticated"))

Expand All @@ -1639,6 +1679,7 @@ def doctor(args: argparse.Namespace) -> None:
"julesApiKey": {
"present": api_key_present,
},
"julesApiValidation": api_validation,
"gh": gh_status,
"julesCli": jules_status,
"apiReady": api_ready,
Expand All @@ -1654,6 +1695,8 @@ def doctor(args: argparse.Namespace) -> None:
[
f"dotenv={'yes' if payload['dotenv']['found'] else 'no'}",
f"api_key={'yes' if api_key_present else 'no'}",
f"api_validated={'yes' if api_validation['validated'] else 'no'}",
f"api_status={api_validation['status']}",
f"api_ready={'yes' if api_ready else 'no'}",
f"gh={'yes' if gh_status.get('installed') else 'no'}",
f"gh_auth={'yes' if gh_status.get('authenticated') else 'no'}",
Expand Down Expand Up @@ -1763,6 +1806,11 @@ def build_parser() -> argparse.ArgumentParser:
help="Check .env, API key, gh auth, and Jules CLI readiness in one command.",
)
doctor_parser.add_argument("--compact", action="store_true", help="Print a short status line instead of JSON.")
doctor_parser.add_argument(
"--validate-api",
action="store_true",
help="Probe the Jules API to verify that the current API key can authenticate.",
)
doctor_parser.set_defaults(func=doctor)

gh_auth_check_parser = subparsers.add_parser(
Expand Down
60 changes: 57 additions & 3 deletions tests/test_jules_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,8 +239,8 @@ def test_close_merged_session_refuses_caution_without_override(self) -> None:

self.assertEqual(1, api_request.call_count)

def test_doctor_reports_api_ready_without_merge_ready(self) -> None:
args = argparse.Namespace(compact=True)
def test_doctor_reports_api_key_present_without_api_ready_when_not_validated(self) -> None:
args = argparse.Namespace(compact=True, validate_api=False)
output = io.StringIO()

with mock.patch.object(jules_api, "find_dotenv_path", return_value=Path("/tmp/.env")):
Expand All @@ -255,11 +255,65 @@ def test_doctor_reports_api_ready_without_merge_ready(self) -> None:
jules_api.doctor(args)

rendered = output.getvalue().strip()
self.assertIn("api_ready=yes", rendered)
self.assertIn("api_key=yes", rendered)
self.assertIn("api_validated=no", rendered)
self.assertIn("api_ready=no", rendered)
self.assertIn("cli_ready=no", rendered)
self.assertIn("merge_ready=no", rendered)
self.assertIn("ready=no", rendered)

def test_doctor_validate_api_marks_api_ready_after_successful_probe(self) -> None:
args = argparse.Namespace(compact=True, validate_api=True)
output = io.StringIO()

with mock.patch.object(jules_api, "find_dotenv_path", return_value=Path("/tmp/.env")):
with mock.patch.dict(jules_api.os.environ, {"JULES_API_KEY": "test-key"}, clear=False):
with mock.patch.object(jules_api, "collect_gh_auth_status", return_value={"installed": True, "authenticated": False}):
with mock.patch.object(
jules_api,
"collect_jules_cli_status",
return_value={"installed": False, "path": None, "authStatus": "not_installed", "ready": False},
):
with mock.patch.object(jules_api, "api_request", return_value={"sources": []}) as api_request:
with redirect_stdout(output):
jules_api.doctor(args)

api_request.assert_called_once_with("GET", "/sources", query={"pageSize": 1})
rendered = output.getvalue().strip()
self.assertIn("api_validated=yes", rendered)
self.assertIn("api_status=ok", rendered)
self.assertIn("api_ready=yes", rendered)
self.assertIn("ready=yes", rendered)

def test_doctor_validate_api_reports_failed_probe_without_ready(self) -> None:
args = argparse.Namespace(compact=True, validate_api=True)
output = io.StringIO()

with mock.patch.object(jules_api, "find_dotenv_path", return_value=Path("/tmp/.env")):
with mock.patch.dict(jules_api.os.environ, {"JULES_API_KEY": "test-key"}, clear=False):
with mock.patch.object(jules_api, "collect_gh_auth_status", return_value={"installed": True, "authenticated": False}):
with mock.patch.object(
jules_api,
"collect_jules_cli_status",
return_value={"installed": False, "path": None, "authStatus": "not_installed", "ready": False},
):
with mock.patch.object(jules_api, "api_request", side_effect=SystemExit(1)):
with redirect_stdout(output):
jules_api.doctor(args)

rendered = output.getvalue().strip()
self.assertIn("api_validated=yes", rendered)
self.assertIn("api_status=failed", rendered)
self.assertIn("api_ready=no", rendered)
self.assertIn("ready=no", rendered)

def test_doctor_parser_supports_api_validation_probe(self) -> None:
parser = jules_api.build_parser()

args = parser.parse_args(["doctor", "--validate-api"])

self.assertTrue(args.validate_api)

def test_collect_jules_cli_status_marks_authenticated_when_probe_succeeds(self) -> None:
version_result = mock.Mock(stdout="1.2.3\n", stderr="")
probe_result = mock.Mock(stdout="repo-a\nrepo-b\n", stderr="")
Expand Down