From 3373153c3966cd24fe6d69635da634a182f517a5 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 16:14:20 +0200 Subject: [PATCH 01/33] mock: test agent PR draft --- .github/workflows/receive-endpoint-event.yml | 235 +++++++ supported-endpoints.json | 643 +++++++++++++++++++ 2 files changed, 878 insertions(+) create mode 100644 .github/workflows/receive-endpoint-event.yml create mode 100644 supported-endpoints.json diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml new file mode 100644 index 00000000..817f3f43 --- /dev/null +++ b/.github/workflows/receive-endpoint-event.yml @@ -0,0 +1,235 @@ +name: Receive endpoint change and generate CLI PR + +on: + repository_dispatch: + types: [endpoint-changed] + workflow_dispatch: + inputs: + mock_payload_json: + description: "Full client_payload JSON (paste the mock payload)" + required: false + default: '' + mock_endpoint_path: + description: "Quick mock — endpoint path (e.g. /projects/{id}/members)" + required: false + default: '/projects/{id}/members' + change_type: + description: "added, modified, removed, or refactored" + required: false + default: 'added' + +jobs: + generate-pr: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.AGENT_PR_PAT }} + fetch-depth: 0 + + - name: Resolve payload + id: payload + env: + # Use env vars to safely pass user inputs — avoids single-quote injection + # when the JSON payload contains quotes. + MOCK_PAYLOAD_JSON: ${{ inputs.mock_payload_json }} + MOCK_ENDPOINT_PATH: ${{ inputs.mock_endpoint_path }} + CHANGE_TYPE: ${{ inputs.change_type }} + run: | + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + if [ -n "$MOCK_PAYLOAD_JSON" ]; then + # Write via env var — safe against single-quote injection + printf '%s' "$MOCK_PAYLOAD_JSON" > /tmp/payload.json + else + python3 -c " + import json, os + payload = { + 'relevant': True, + 'source_pr_url': 'mock', + 'changed_endpoints': [{ + 'method': 'GET', + 'path': os.environ['MOCK_ENDPOINT_PATH'], + 'change_type': os.environ['CHANGE_TYPE'], + 'auth': ['userApiToken'], + 'summary': 'Mock endpoint for testing' + }] + } + print(json.dumps(payload)) + " > /tmp/payload.json + fi + else + # repository_dispatch: client_payload is already JSON — write via tojson filter + cat <<'EOJSON' > /tmp/payload.json + ${{ toJson(github.event.client_payload) }} + EOJSON + fi + + - name: Call AI agent and write files + env: + # Built-in token — no secret needed. GitHub Models is available to Copilot-licensed orgs. + GITHUB_TOKEN: ${{ github.token }} + run: | + python3 - <<'EOF' + import json, urllib.request, os, pathlib + + payload = json.load(open("/tmp/payload.json")) + + # Skip if the detected change is not relevant to this repo + if not payload.get("relevant", True): + print("Payload marked as not relevant — skipping.") + import sys; sys.exit(0) + + endpoints = payload["changed_endpoints"] + manifest = json.load(open("supported-endpoints.json")) + + # Read repo context inline — no helper scripts needed. + # cloudos_cli/clos.py is the primary command implementation file. + # The project uses cloudos_cli/_version.py (not pyproject.toml) for versioning. + style_ref = open("cloudos_cli/clos.py").read()[:4000] # first 4000 chars as style guide + test_ref = open(next(pathlib.Path("tests").glob("test_*.py"))).read() + changelog = open("CHANGELOG.md").read()[-3000:] # last ~3000 chars + version_raw = open("cloudos_cli/_version.py").read() + # version_raw is like: __version__ = '2.91.0' + version = version_raw.strip() + + prompt = f"""You are an expert Python developer maintaining cloudos-cli, + a CLI tool that wraps a REST API. An API endpoint has changed and you must + update the CLI to reflect it. + + CHANGE DETAILS: + {json.dumps(endpoints, indent=2)} + + Change types you may encounter and how to handle them: + - added: create a new CLI command and register it + - modified: update the existing command, its parameters and/or output handling + - removed: delete or deprecate the existing CLI command gracefully + - refactored: update internals only — keep the CLI interface identical + + STYLE REFERENCE (follow this exactly): + {style_ref} + + TEST REFERENCE (follow this pattern): + {test_ref} + + CHANGELOG FORMAT (match this exactly): + {changelog} + + CURRENT VERSION LINE: + {version} + + CURRENT SUPPORTED ENDPOINTS MANIFEST: + {json.dumps(manifest, indent=2)} + + PROJECT STRUCTURE NOTES: + - Commands are implemented as methods in cloudos_cli/clos.py or in + module files under cloudos_cli// (e.g. cloudos_cli/jobs/job.py, + cloudos_cli/datasets/datasets.py, cloudos_cli/procurement/images.py). + There is NO cloudos_cli/commands/ directory — do not create one. + - Version is stored in cloudos_cli/_version.py as: __version__ = 'X.Y.Z' + There is NO pyproject.toml — do not create one. + - Tests live under tests/ and use pytest + responses + mock libraries. + + INSTRUCTIONS: + - Increment the patch version in cloudos_cli/_version.py + - Add a CHANGELOG entry at the top of CHANGELOG.md matching the format above + - Create or modify the relevant module file under cloudos_cli// + - Write at least 2 unit tests in tests/ + - Update supported-endpoints.json to reflect the change + - For removed endpoints: add a deprecation notice in the changelog, remove + the command implementation, and remove the entry from supported-endpoints.json + + Respond with ONLY a JSON object — no explanation, no markdown fences: + {{ + "pr_title": "feat|fix|chore: short description", + "pr_body": "markdown PR description", + "files": [ + {{ + "path": "relative/path/from/repo/root.py", + "action": "create|modify|delete", + "content": "full file content as string (omit for delete)" + }} + ] + }} + """ + + # GitHub Models — OpenAI-compatible chat completions, authenticated with GITHUB_TOKEN. + # Find available model IDs at: github.com/marketplace/models + body = json.dumps({ + "model": "claude-3-7-sonnet", + "max_tokens": 16000, + "messages": [{"role": "user", "content": prompt}] + }).encode() + + req = urllib.request.Request( + "https://models.inference.ai.azure.com/chat/completions", + data=body, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}" + } + ) + with urllib.request.urlopen(req) as r: + result = json.loads(r.read()) + + # OpenAI format: choices[0].message.content + raw_text = result["choices"][0]["message"]["content"].strip() + # Strip markdown code fences if the model wrapped the JSON response + if raw_text.startswith("```"): + raw_text = raw_text.split("\n", 1)[1] # drop the ```json line + raw_text = raw_text.rsplit("```", 1)[0].strip() + response = json.loads(raw_text) + + # Write generated files + for f in response["files"]: + p = pathlib.Path(f["path"]) + if f["action"] == "delete": + p.unlink(missing_ok=True) + else: + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(f["content"]) + + # Write PR metadata for subsequent steps + open("/tmp/pr-title.txt", "w").write(response["pr_title"]) + open("/tmp/pr-body.md", "w").write(response["pr_body"]) + EOF + + - name: Create branch and commit + id: branch + run: | + SLUG=$(echo '${{ inputs.mock_endpoint_path || github.event.client_payload.changed_endpoints[0].path }}' \ + | tr '/' '-' | tr -d '{}' | sed 's/^-//') + BRANCH="agent/endpoint-$(date +%Y%m%d%H%M%S)-${SLUG}" + git config user.name "cloudos-agent[bot]" + git config user.email "cloudos-agent@users.noreply.github.com" + git checkout -b "$BRANCH" + git add -A + # Only commit if there are actual changes; exit cleanly otherwise + if git diff --cached --quiet; then + echo "No files were changed by the agent — nothing to commit." + exit 1 + fi + git commit -m "$(cat /tmp/pr-title.txt)" + git push origin "$BRANCH" + echo "branch=$BRANCH" >> $GITHUB_OUTPUT + + - name: Open draft PR + env: + GH_TOKEN: ${{ secrets.AGENT_PR_PAT }} + run: | + gh pr create \ + --draft \ + --base main \ + --head "${{ steps.branch.outputs.branch }}" \ + --title "$(cat /tmp/pr-title.txt)" \ + --body-file /tmp/pr-body.md \ + --label "agent-generated" + + - name: Open fallback issue on failure + if: failure() + env: + GH_TOKEN: ${{ secrets.AGENT_PR_PAT }} + run: | + gh issue create \ + --title "Agent failed: endpoint change requires manual wrapping" \ + --body "The agent workflow failed. Trigger payload: $(cat /tmp/payload.json)" \ + --label "agent-failed" \ No newline at end of file diff --git a/supported-endpoints.json b/supported-endpoints.json new file mode 100644 index 00000000..f221f0f5 --- /dev/null +++ b/supported-endpoints.json @@ -0,0 +1,643 @@ +{ + "version": "1.0", + "cli_version": "2.91.0", + "description": "Single source of truth mapping every API endpoint wrapped by cloudos-cli to the CLI command that wraps it. Used by the AI agent PR workflow to detect breaking changes in the api-server repo.", + "notes": [ + "Path parameters are expressed as {param_name}.", + "Query parameters (teamId, page, limit, etc.) are omitted from 'path'; they appear in 'query_params'.", + "Entries with cli_command starting with '_internal/' are not directly user-facing but are called as helpers during a user command.", + "added_in_version reflects the CLI release when this mapping was first formally tracked (2.91.0). Historical endpoint additions were not back-filled." + ], + "endpoints": [ + { + "id": "jobs-run", + "method": "POST", + "path": "/api/v2/jobs", + "query_params": ["teamId"], + "cli_command": "cloudos job run", + "source_file": "cloudos_cli/jobs/job.py", + "added_in_version": "2.91.0", + "notes": "Also called by 'cloudos bash job' and 'cloudos bash array-job'" + }, + { + "id": "jobs-array-metadata", + "method": "GET", + "path": "/api/v1/jobs/array-file/metadata", + "query_params": [], + "cli_command": "cloudos job run", + "source_file": "cloudos_cli/jobs/job.py", + "added_in_version": "2.91.0", + "notes": "Called when submitting array jobs. Also called by 'cloudos bash array-job'" + }, + { + "id": "jobs-status", + "method": "GET", + "path": "/api/v1/jobs/{job_id}", + "query_params": ["teamId"], + "cli_command": "cloudos job status", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": "Also used internally by 'cloudos job logs', 'cloudos job results', 'cloudos job workdir', 'cloudos job related'" + }, + { + "id": "jobs-details", + "method": "GET", + "path": "/api/v1/jobs/{job_id}", + "query_params": ["teamId"], + "cli_command": "cloudos job details", + "source_file": "cloudos_cli/jobs/job.py", + "added_in_version": "2.91.0", + "notes": "Same endpoint as status but surfaced separately by the 'details' sub-command" + }, + { + "id": "jobs-list", + "method": "GET", + "path": "/api/v2/jobs", + "query_params": ["teamId", "page", "limit"], + "cli_command": "cloudos job list", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "jobs-list-related-by-workdir", + "method": "GET", + "path": "/api/v2/jobs", + "query_params": ["workDirectory.folderId", "teamId"], + "cli_command": "cloudos job related", + "source_file": "cloudos_cli/jobs/job.py", + "added_in_version": "2.91.0", + "notes": "Filters jobs by shared work-directory folder to find related analyses" + }, + { + "id": "jobs-abort", + "method": "PUT", + "path": "/api/v2/jobs/{job_id}/abort", + "query_params": ["forceAbort", "teamId"], + "cli_command": "cloudos job abort", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "jobs-archive", + "method": "PUT", + "path": "/api/v1/jobs", + "query_params": ["teamId"], + "cli_command": "cloudos job archive", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": "Also called by 'cloudos job unarchive'. Body payload differentiates the action." + }, + { + "id": "jobs-delete-results", + "method": "DELETE", + "path": "/api/v1/jobs/{job_id}/data", + "query_params": ["properties[]", "teamId"], + "cli_command": "cloudos job workdir", + "source_file": "cloudos_cli/jobs/job.py", + "added_in_version": "2.91.0", + "notes": "Called when --delete flag is passed to 'cloudos job workdir'" + }, + { + "id": "jobs-clone-resume-payload", + "method": "GET", + "path": "/api/v1/jobs/{job_id}/request-payload", + "query_params": ["teamId"], + "cli_command": "cloudos job clone", + "source_file": "cloudos_cli/jobs/job.py", + "added_in_version": "2.91.0", + "notes": "Also called by 'cloudos job resume'" + }, + { + "id": "jobs-clone-resume-submit", + "method": "POST", + "path": "/api/v2/jobs", + "query_params": ["teamId"], + "cli_command": "cloudos job resume", + "source_file": "cloudos_cli/jobs/job.py", + "added_in_version": "2.91.0", + "notes": "POST after fetching the request-payload; also used by 'cloudos job clone'" + }, + { + "id": "jobs-git-branches", + "method": "GET", + "path": "/api/v1/git/{strategy}/getBranches", + "query_params": [], + "cli_command": "cloudos job clone", + "source_file": "cloudos_cli/jobs/job.py", + "added_in_version": "2.91.0", + "notes": "Used when resolving workflow branches for clone/resume. Also called by 'cloudos job resume'" + }, + { + "id": "jobs-cost", + "method": "GET", + "path": "/api/v1/jobs/{job_id}/costs/compute", + "query_params": [], + "cli_command": "cloudos job cost", + "source_file": "cloudos_cli/cost/cost.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "jobs-workdir-folder-lookup", + "method": "GET", + "path": "/api/v1/folders/", + "query_params": ["id", "teamId"], + "cli_command": "cloudos job workdir", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": "Also used internally to resolve folder status for 'cloudos job related'" + }, + { + "id": "bash-job", + "method": "POST", + "path": "/api/v2/jobs", + "query_params": ["teamId"], + "cli_command": "cloudos bash job", + "source_file": "cloudos_cli/jobs/job.py", + "added_in_version": "2.91.0", + "notes": "Bash job submission" + }, + { + "id": "bash-array-job-metadata", + "method": "GET", + "path": "/api/v1/jobs/array-file/metadata", + "query_params": [], + "cli_command": "cloudos bash array-job", + "source_file": "cloudos_cli/jobs/job.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "bash-array-job-submit", + "method": "POST", + "path": "/api/v2/jobs", + "query_params": ["teamId"], + "cli_command": "cloudos bash array-job", + "source_file": "cloudos_cli/jobs/job.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "queue-list-user", + "method": "GET", + "path": "/api/v1/teams/aws/v2/job-queues", + "query_params": ["teamId"], + "cli_command": "cloudos queue list", + "source_file": "cloudos_cli/queue/queue.py", + "added_in_version": "2.91.0", + "notes": "Returns user-visible / team job queues" + }, + { + "id": "queue-list-system", + "method": "GET", + "path": "/api/v1/teams/aws/v2/system-job-queues", + "query_params": ["teamId"], + "cli_command": "cloudos queue list", + "source_file": "cloudos_cli/queue/queue.py", + "added_in_version": "2.91.0", + "notes": "Returns system-level job queues; results merged with user queues in the CLI output" + }, + { + "id": "projects-list", + "method": "GET", + "path": "/api/v2/projects", + "query_params": ["teamId", "pageSize", "page"], + "cli_command": "cloudos project list", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": "Also accepts 'search' query param when filtering by name" + }, + { + "id": "projects-create", + "method": "POST", + "path": "/api/v1/projects", + "query_params": ["teamId"], + "cli_command": "cloudos project create", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "workflows-list", + "method": "GET", + "path": "/api/v3/workflows", + "query_params": ["teamId", "pageSize", "page", "archived.status"], + "cli_command": "cloudos workflow list", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": "Also accepts 'search' query param" + }, + { + "id": "workflows-import", + "method": "POST", + "path": "/api/v2/workflows", + "query_params": ["teamId"], + "cli_command": "cloudos workflow import", + "source_file": "cloudos_cli/import_wf/import_wf.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "workflows-import-legacy", + "method": "POST", + "path": "/api/v1/workflows", + "query_params": ["teamId"], + "cli_command": "cloudos workflow import", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": "Legacy v1 fallback path for workflow import" + }, + { + "id": "workflows-import-get-public-repo", + "method": "GET", + "path": "/api/v1/git/{platform}/getPublicRepo", + "query_params": [], + "cli_command": "cloudos workflow import", + "source_file": "cloudos_cli/import_wf/import_wf.py", + "added_in_version": "2.91.0", + "notes": "Fetches metadata for a public git repository before import" + }, + { + "id": "workflows-import-get-config", + "method": "GET", + "path": "/api/v1/git/{platform}/getWorkflowConfig/{repo_name}/{repo_owner}", + "query_params": [], + "cli_command": "cloudos workflow import", + "source_file": "cloudos_cli/import_wf/import_wf.py", + "added_in_version": "2.91.0", + "notes": "Fetches the workflow main-file config for the repository" + }, + { + "id": "cromwell-status", + "method": "GET", + "path": "/api/v1/cromwell", + "query_params": ["teamId"], + "cli_command": "cloudos cromwell status", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "cromwell-start", + "method": "PUT", + "path": "/api/v1/cromwell/restart", + "query_params": ["teamId"], + "cli_command": "cloudos cromwell start", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "cromwell-stop", + "method": "PUT", + "path": "/api/v1/cromwell/stop", + "query_params": ["teamId"], + "cli_command": "cloudos cromwell stop", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "datasets-ls-project", + "method": "GET", + "path": "/api/v2/datasets", + "query_params": ["projectId", "teamId"], + "cli_command": "cloudos datasets ls", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Lists top-level dataset folders within a project" + }, + { + "id": "datasets-ls-folder-items", + "method": "GET", + "path": "/api/v1/datasets/{folder_id}/items", + "query_params": ["teamId"], + "cli_command": "cloudos datasets ls", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Lists items inside a dataset folder" + }, + { + "id": "datasets-ls-s3", + "method": "GET", + "path": "/api/v1/data-access/s3/bucket-contents", + "query_params": ["bucket", "path", "teamId"], + "cli_command": "cloudos datasets ls", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Lists S3 bucket contents for a given path" + }, + { + "id": "datasets-ls-azure", + "method": "GET", + "path": "/api/v1/data-access/azure/container-contents", + "query_params": ["containerName", "path", "teamId"], + "cli_command": "cloudos datasets ls", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Lists Azure Blob Storage container contents" + }, + { + "id": "datasets-ls-virtual-folder", + "method": "GET", + "path": "/api/v1/folders/virtual/{folder_id}/items", + "query_params": ["teamId"], + "cli_command": "cloudos datasets ls", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Lists items inside a virtual folder" + }, + { + "id": "datasets-mv", + "method": "PUT", + "path": "/api/v1/dataItems/move", + "query_params": ["teamId"], + "cli_command": "cloudos datasets mv", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Moves files or folders to a different location" + }, + { + "id": "datasets-rename-file", + "method": "PUT", + "path": "/api/v1/files/{item_id}", + "query_params": ["teamId"], + "cli_command": "cloudos datasets rename", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Renames a file item" + }, + { + "id": "datasets-rename-folder", + "method": "PUT", + "path": "/api/v1/folders/{item_id}", + "query_params": ["teamId"], + "cli_command": "cloudos datasets rename", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Renames a folder item" + }, + { + "id": "datasets-cp-virtual-folder", + "method": "POST", + "path": "/api/v1/folders/virtual", + "query_params": ["teamId"], + "cli_command": "cloudos datasets cp", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Creates a virtual folder copy. Also called by 'cloudos datasets mkdir'" + }, + { + "id": "datasets-cp-s3-folder", + "method": "POST", + "path": "/api/v1/folders/s3", + "query_params": ["teamId"], + "cli_command": "cloudos datasets cp", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Copies an S3 folder into a dataset" + }, + { + "id": "datasets-cp-s3-file", + "method": "POST", + "path": "/api/v1/files/s3", + "query_params": ["teamId"], + "cli_command": "cloudos datasets cp", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Copies an S3 file into a dataset" + }, + { + "id": "datasets-cp-azure-folder", + "method": "POST", + "path": "/api/v1/folders/azure-blob", + "query_params": ["teamId"], + "cli_command": "cloudos datasets cp", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Copies an Azure Blob folder into a dataset" + }, + { + "id": "datasets-cp-azure-file", + "method": "POST", + "path": "/api/v1/files/azure-blob", + "query_params": ["teamId"], + "cli_command": "cloudos datasets cp", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Copies an Azure Blob file into a dataset" + }, + { + "id": "datasets-mkdir", + "method": "POST", + "path": "/api/v1/folders/virtual", + "query_params": ["teamId"], + "cli_command": "cloudos datasets mkdir", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Creates a new virtual folder" + }, + { + "id": "datasets-rm-file", + "method": "DELETE", + "path": "/api/v1/files/{item_id}", + "query_params": ["teamId"], + "cli_command": "cloudos datasets rm", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Deletes a file from a dataset" + }, + { + "id": "datasets-rm-folder", + "method": "DELETE", + "path": "/api/v1/folders/{item_id}", + "query_params": ["teamId"], + "cli_command": "cloudos datasets rm", + "source_file": "cloudos_cli/datasets/datasets.py", + "added_in_version": "2.91.0", + "notes": "Deletes a folder from a dataset" + }, + { + "id": "datasets-link-mount-v2", + "method": "POST", + "path": "/api/v2/interactive-sessions/{session_id}/fuse-filesystem/mount", + "query_params": ["teamId"], + "cli_command": "cloudos datasets link", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": "Also called by 'cloudos link'. Falls back to v1 on error." + }, + { + "id": "datasets-link-mount-v1-fallback", + "method": "POST", + "path": "/api/v1/interactive-sessions/{session_id}/fuse-filesystem/mount", + "query_params": ["teamId"], + "cli_command": "cloudos datasets link", + "source_file": "cloudos_cli/link/link.py", + "added_in_version": "2.91.0", + "notes": "Legacy v1 fallback. Also used by 'cloudos link'." + }, + { + "id": "datasets-link-verify", + "method": "GET", + "path": "/api/v1/interactive-sessions/{session_id}/fuse-filesystems", + "query_params": ["teamId"], + "cli_command": "cloudos datasets link", + "source_file": "cloudos_cli/link/link.py", + "added_in_version": "2.91.0", + "notes": "Polls mount status after requesting a link. Also used by 'cloudos link'." + }, + { + "id": "interactive-session-list", + "method": "GET", + "path": "/api/v3/interactive-sessions", + "query_params": ["teamId", "page", "limit"], + "cli_command": "cloudos interactive-session list", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "interactive-session-create", + "method": "POST", + "path": "/api/v1/interactive-sessions", + "query_params": ["teamId"], + "cli_command": "cloudos interactive-session create", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "interactive-session-status", + "method": "GET", + "path": "/api/v2/interactive-sessions/{session_id}", + "query_params": ["teamId"], + "cli_command": "cloudos interactive-session status", + "source_file": "cloudos_cli/interactive_session/interactive_session.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "interactive-session-resume", + "method": "PUT", + "path": "/api/v1/interactive-sessions/{session_id}/resume", + "query_params": ["teamId"], + "cli_command": "cloudos interactive-session resume", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "interactive-session-pause", + "method": "PUT", + "path": "/api/v1/interactive-sessions/{session_id}/abort", + "query_params": ["teamId"], + "cli_command": "cloudos interactive-session pause", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "link-mount-v2", + "method": "POST", + "path": "/api/v2/interactive-sessions/{session_id}/fuse-filesystem/mount", + "query_params": ["teamId"], + "cli_command": "cloudos link", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": "Primary mount call; falls back to v1 on failure" + }, + { + "id": "link-mount-v1-fallback", + "method": "POST", + "path": "/api/v1/interactive-sessions/{session_id}/fuse-filesystem/mount", + "query_params": ["teamId"], + "cli_command": "cloudos link", + "source_file": "cloudos_cli/link/link.py", + "added_in_version": "2.91.0", + "notes": "Legacy v1 fallback path" + }, + { + "id": "link-verify", + "method": "GET", + "path": "/api/v1/interactive-sessions/{session_id}/fuse-filesystems", + "query_params": ["teamId"], + "cli_command": "cloudos link", + "source_file": "cloudos_cli/link/link.py", + "added_in_version": "2.91.0", + "notes": "Polls mount status after requesting a link" + }, + { + "id": "procurement-images-list", + "method": "GET", + "path": "/api/v1/procurements/{procurement_id}/images", + "query_params": ["page", "limit"], + "cli_command": "cloudos procurement images list", + "source_file": "cloudos_cli/procurement/images.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "procurement-images-set", + "method": "PUT", + "path": "/api/v1/procurements/{procurement_id}/images", + "query_params": [], + "cli_command": "cloudos procurement images set", + "source_file": "cloudos_cli/procurement/images.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "procurement-images-reset", + "method": "PUT", + "path": "/api/v1/procurements/{procurement_id}/images/reset", + "query_params": [], + "cli_command": "cloudos procurement images reset", + "source_file": "cloudos_cli/procurement/images.py", + "added_in_version": "2.91.0", + "notes": null + }, + { + "id": "_internal-cloud-azure-detect", + "method": "GET", + "path": "/api/v1/cloud/azure", + "query_params": ["teamId"], + "cli_command": "_internal/cloud-provider-detection", + "source_file": "cloudos_cli/utils/cloud.py", + "added_in_version": "2.91.0", + "notes": "Called internally to detect whether the workspace is running on Azure; affects which dataset copy endpoints are used" + }, + { + "id": "_internal-users-me", + "method": "GET", + "path": "/api/v1/users/me", + "query_params": [], + "cli_command": "_internal/current-user", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": "Called internally to resolve the current authenticated user" + }, + { + "id": "_internal-users-search-assist", + "method": "GET", + "path": "/api/v1/users/search-assist", + "query_params": ["q", "teamId"], + "cli_command": "_internal/user-search", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": "Called internally to resolve a username/email to a user ID" + }, + { + "id": "_internal-folders-status", + "method": "GET", + "path": "/api/v1/folders/", + "query_params": ["id", "status[]", "teamId"], + "cli_command": "_internal/folder-deletion-status", + "source_file": "cloudos_cli/clos.py", + "added_in_version": "2.91.0", + "notes": "Called internally to poll folder deletion status during 'cloudos job workdir' and dataset operations" + } + ] +} From d37b8f239dc07053b11da5a1437693d864c3477d Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 17:01:00 +0200 Subject: [PATCH 02/33] chore: trigger test dispatch --- .github/test-trigger.json | 13 +++++++++++++ .github/workflows/receive-endpoint-event.yml | 18 +++++++++++++++--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 .github/test-trigger.json diff --git a/.github/test-trigger.json b/.github/test-trigger.json new file mode 100644 index 00000000..60943cb6 --- /dev/null +++ b/.github/test-trigger.json @@ -0,0 +1,13 @@ +{ + "relevant": true, + "source_pr_url": "mock", + "changed_endpoints": [ + { + "method": "GET", + "path": "/api/v1/projects/{id}/members", + "change_type": "added", + "auth": ["userApiToken"], + "summary": "Returns members of a project" + } + ] +} diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 817f3f43..fda31b94 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -17,6 +17,11 @@ on: description: "added, modified, removed, or refactored" required: false default: 'added' + push: + branches: + - ai-server-cli + paths: + - '.github/test-trigger.json' jobs: generate-pr: @@ -36,7 +41,10 @@ jobs: MOCK_ENDPOINT_PATH: ${{ inputs.mock_endpoint_path }} CHANGE_TYPE: ${{ inputs.change_type }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + if [ "${{ github.event_name }}" = "push" ]; then + # Sentinel-file trigger: payload is read directly from the committed file + cp .github/test-trigger.json /tmp/payload.json + elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then if [ -n "$MOCK_PAYLOAD_JSON" ]; then # Write via env var — safe against single-quote injection printf '%s' "$MOCK_PAYLOAD_JSON" > /tmp/payload.json @@ -196,8 +204,12 @@ jobs: - name: Create branch and commit id: branch run: | - SLUG=$(echo '${{ inputs.mock_endpoint_path || github.event.client_payload.changed_endpoints[0].path }}' \ - | tr '/' '-' | tr -d '{}' | sed 's/^-//') + # Extract the first endpoint path from the resolved payload (works for all trigger types) + SLUG=$(python3 -c " +import json, sys +p = json.load(open('/tmp/payload.json')) +print(p['changed_endpoints'][0]['path']) +" | tr '/' '-' | tr -d '{}' | sed 's/^-//') BRANCH="agent/endpoint-$(date +%Y%m%d%H%M%S)-${SLUG}" git config user.name "cloudos-agent[bot]" git config user.email "cloudos-agent@users.noreply.github.com" From e0d30da25cce70796c3d415204028e39fe99575f Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 17:03:41 +0200 Subject: [PATCH 03/33] fix: collapse multi-line python slug extraction to single line --- .github/workflows/receive-endpoint-event.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index fda31b94..8e452f82 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -205,11 +205,8 @@ jobs: id: branch run: | # Extract the first endpoint path from the resolved payload (works for all trigger types) - SLUG=$(python3 -c " -import json, sys -p = json.load(open('/tmp/payload.json')) -print(p['changed_endpoints'][0]['path']) -" | tr '/' '-' | tr -d '{}' | sed 's/^-//') + SLUG=$(python3 -c "import json; p=json.load(open('/tmp/payload.json')); print(p['changed_endpoints'][0]['path'])" \ + | tr '/' '-' | tr -d '{}' | sed 's/^-//') BRANCH="agent/endpoint-$(date +%Y%m%d%H%M%S)-${SLUG}" git config user.name "cloudos-agent[bot]" git config user.email "cloudos-agent@users.noreply.github.com" From cb978b5d28ca0fc22d98df3354b0500e15505ed5 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 17:06:21 +0200 Subject: [PATCH 04/33] ci: test AI trigger --- .github/test-trigger.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/test-trigger.json b/.github/test-trigger.json index 60943cb6..0e1dd0ac 100644 --- a/.github/test-trigger.json +++ b/.github/test-trigger.json @@ -7,7 +7,7 @@ "path": "/api/v1/projects/{id}/members", "change_type": "added", "auth": ["userApiToken"], - "summary": "Returns members of a project" + "summary": "Returns members of a project " } ] } From 6ad74fc82e35f95d44b60e6c3a1dcedbbe094a16 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 17:11:48 +0200 Subject: [PATCH 05/33] fix: use versioned model ID and add API error body logging --- .github/test-trigger.json | 2 +- .github/workflows/receive-endpoint-event.yml | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/test-trigger.json b/.github/test-trigger.json index 0e1dd0ac..60943cb6 100644 --- a/.github/test-trigger.json +++ b/.github/test-trigger.json @@ -7,7 +7,7 @@ "path": "/api/v1/projects/{id}/members", "change_type": "added", "auth": ["userApiToken"], - "summary": "Returns members of a project " + "summary": "Returns members of a project" } ] } diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 8e452f82..83eead9d 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -163,7 +163,7 @@ jobs: # GitHub Models — OpenAI-compatible chat completions, authenticated with GITHUB_TOKEN. # Find available model IDs at: github.com/marketplace/models body = json.dumps({ - "model": "claude-3-7-sonnet", + "model": "claude-3-7-sonnet-20250219", "max_tokens": 16000, "messages": [{"role": "user", "content": prompt}] }).encode() @@ -176,8 +176,13 @@ jobs: "Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}" } ) - with urllib.request.urlopen(req) as r: - result = json.loads(r.read()) + try: + with urllib.request.urlopen(req) as r: + result = json.loads(r.read()) + except urllib.error.HTTPError as e: + body_bytes = e.read() + print(f"GitHub Models API error {e.code}: {body_bytes.decode()}", flush=True) + raise # OpenAI format: choices[0].message.content raw_text = result["choices"][0]["message"]["content"].strip() From cb1f54623c67b705516bbc58611c8caf026926ff Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 17:26:01 +0200 Subject: [PATCH 06/33] test: use existing model --- .github/test-trigger.json | 2 +- .github/workflows/receive-endpoint-event.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/test-trigger.json b/.github/test-trigger.json index 60943cb6..0e1dd0ac 100644 --- a/.github/test-trigger.json +++ b/.github/test-trigger.json @@ -7,7 +7,7 @@ "path": "/api/v1/projects/{id}/members", "change_type": "added", "auth": ["userApiToken"], - "summary": "Returns members of a project" + "summary": "Returns members of a project " } ] } diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 83eead9d..6b2be78c 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -160,16 +160,16 @@ jobs: }} """ - # GitHub Models — OpenAI-compatible chat completions, authenticated with GITHUB_TOKEN. - # Find available model IDs at: github.com/marketplace/models + # GitHub Models (new catalog endpoint) — OpenAI-compatible, authenticated with GITHUB_TOKEN. + # codestral-2501 is purpose-built for code generation; swap for openai/gpt-4.1 if preferred. body = json.dumps({ - "model": "claude-3-7-sonnet-20250219", + "model": "mistral-ai/codestral-2501", "max_tokens": 16000, "messages": [{"role": "user", "content": prompt}] }).encode() req = urllib.request.Request( - "https://models.inference.ai.azure.com/chat/completions", + "https://models.github.ai/inference/chat/completions", data=body, headers={ "Content-Type": "application/json", From b9d1af1fd2fe6b55c9a92ba3c8258bf465454bac Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 17:28:39 +0200 Subject: [PATCH 07/33] test: use existing model --- .github/test-trigger.json | 2 +- .github/workflows/receive-endpoint-event.yml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/test-trigger.json b/.github/test-trigger.json index 0e1dd0ac..60943cb6 100644 --- a/.github/test-trigger.json +++ b/.github/test-trigger.json @@ -7,7 +7,7 @@ "path": "/api/v1/projects/{id}/members", "change_type": "added", "auth": ["userApiToken"], - "summary": "Returns members of a project " + "summary": "Returns members of a project" } ] } diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 6b2be78c..0c8d353a 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -74,8 +74,9 @@ jobs: - name: Call AI agent and write files env: - # Built-in token — no secret needed. GitHub Models is available to Copilot-licensed orgs. - GITHUB_TOKEN: ${{ github.token }} + # AGENT_PR_PAT is a classic PAT with repo+workflow scope. + # github.token cannot call models.github.ai — a real user PAT is required. + GITHUB_TOKEN: ${{ secrets.AGENT_PR_PAT }} run: | python3 - <<'EOF' import json, urllib.request, os, pathlib From 7b7f6d52f5523ed187bf73c9e7997881dea47f6b Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 17:31:32 +0200 Subject: [PATCH 08/33] mock: use gpt-5 --- .github/test-trigger.json | 2 +- .github/workflows/receive-endpoint-event.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/test-trigger.json b/.github/test-trigger.json index 60943cb6..0e1dd0ac 100644 --- a/.github/test-trigger.json +++ b/.github/test-trigger.json @@ -7,7 +7,7 @@ "path": "/api/v1/projects/{id}/members", "change_type": "added", "auth": ["userApiToken"], - "summary": "Returns members of a project" + "summary": "Returns members of a project " } ] } diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 0c8d353a..cf164195 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -164,7 +164,7 @@ jobs: # GitHub Models (new catalog endpoint) — OpenAI-compatible, authenticated with GITHUB_TOKEN. # codestral-2501 is purpose-built for code generation; swap for openai/gpt-4.1 if preferred. body = json.dumps({ - "model": "mistral-ai/codestral-2501", + "model": "openai/gpt-5", "max_tokens": 16000, "messages": [{"role": "user", "content": prompt}] }).encode() From 850bde44e6a5356290fa8ba9cc960b02c469754c Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 18:00:31 +0200 Subject: [PATCH 09/33] mock: use gpt-4.1 --- .github/test-trigger.json | 2 +- .github/workflows/receive-endpoint-event.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/test-trigger.json b/.github/test-trigger.json index 0e1dd0ac..60943cb6 100644 --- a/.github/test-trigger.json +++ b/.github/test-trigger.json @@ -7,7 +7,7 @@ "path": "/api/v1/projects/{id}/members", "change_type": "added", "auth": ["userApiToken"], - "summary": "Returns members of a project " + "summary": "Returns members of a project" } ] } diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index cf164195..8dbda4f8 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -162,10 +162,10 @@ jobs: """ # GitHub Models (new catalog endpoint) — OpenAI-compatible, authenticated with GITHUB_TOKEN. - # codestral-2501 is purpose-built for code generation; swap for openai/gpt-4.1 if preferred. + # gpt-4.1: 1M input tokens, 32K output — best for large context code generation. body = json.dumps({ - "model": "openai/gpt-5", - "max_tokens": 16000, + "model": "openai/gpt-4.1", + "max_tokens": 32768, "messages": [{"role": "user", "content": prompt}] }).encode() From 9b68e708330d0fd02c132ef0b7ee17880302bc30 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 18:11:08 +0200 Subject: [PATCH 10/33] mock: go back to azureml --- .github/test-trigger.json | 2 +- .github/workflows/receive-endpoint-event.yml | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/test-trigger.json b/.github/test-trigger.json index 60943cb6..0e1dd0ac 100644 --- a/.github/test-trigger.json +++ b/.github/test-trigger.json @@ -7,7 +7,7 @@ "path": "/api/v1/projects/{id}/members", "change_type": "added", "auth": ["userApiToken"], - "summary": "Returns members of a project" + "summary": "Returns members of a project " } ] } diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 8dbda4f8..caa46e7c 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -161,16 +161,15 @@ jobs: }} """ - # GitHub Models (new catalog endpoint) — OpenAI-compatible, authenticated with GITHUB_TOKEN. - # gpt-4.1: 1M input tokens, 32K output — best for large context code generation. + # Testing models.inference.ai.azure.com with gpt-4o body = json.dumps({ - "model": "openai/gpt-4.1", + "model": "gpt-4o", "max_tokens": 32768, "messages": [{"role": "user", "content": prompt}] }).encode() req = urllib.request.Request( - "https://models.github.ai/inference/chat/completions", + "https://models.inference.ai.azure.com/chat/completions", data=body, headers={ "Content-Type": "application/json", From 04d0b59d663ba6dc9a2e167b72e945cea327f994 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 18:50:34 +0200 Subject: [PATCH 11/33] refactor: use copilot sessions not models --- .github/test-trigger.json | 2 +- .github/workflows/receive-endpoint-event.yml | 219 ++++++------------- 2 files changed, 69 insertions(+), 152 deletions(-) diff --git a/.github/test-trigger.json b/.github/test-trigger.json index 0e1dd0ac..60943cb6 100644 --- a/.github/test-trigger.json +++ b/.github/test-trigger.json @@ -7,7 +7,7 @@ "path": "/api/v1/projects/{id}/members", "change_type": "added", "auth": ["userApiToken"], - "summary": "Returns members of a project " + "summary": "Returns members of a project" } ] } diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index caa46e7c..8bec5eb3 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -72,171 +72,88 @@ jobs: EOJSON fi - - name: Call AI agent and write files - env: - # AGENT_PR_PAT is a classic PAT with repo+workflow scope. - # github.token cannot call models.github.ai — a real user PAT is required. - GITHUB_TOKEN: ${{ secrets.AGENT_PR_PAT }} + - name: Build Copilot issue body run: | python3 - <<'EOF' - import json, urllib.request, os, pathlib + import json, pathlib, textwrap - payload = json.load(open("/tmp/payload.json")) + payload = json.load(open("/tmp/payload.json")) - # Skip if the detected change is not relevant to this repo if not payload.get("relevant", True): print("Payload marked as not relevant — skipping.") import sys; sys.exit(0) endpoints = payload["changed_endpoints"] + source_pr = payload.get("source_pr_url", "manual trigger") manifest = json.load(open("supported-endpoints.json")) - - # Read repo context inline — no helper scripts needed. - # cloudos_cli/clos.py is the primary command implementation file. - # The project uses cloudos_cli/_version.py (not pyproject.toml) for versioning. - style_ref = open("cloudos_cli/clos.py").read()[:4000] # first 4000 chars as style guide - test_ref = open(next(pathlib.Path("tests").glob("test_*.py"))).read() - changelog = open("CHANGELOG.md").read()[-3000:] # last ~3000 chars - version_raw = open("cloudos_cli/_version.py").read() - # version_raw is like: __version__ = '2.91.0' - version = version_raw.strip() - - prompt = f"""You are an expert Python developer maintaining cloudos-cli, - a CLI tool that wraps a REST API. An API endpoint has changed and you must - update the CLI to reflect it. - - CHANGE DETAILS: - {json.dumps(endpoints, indent=2)} - - Change types you may encounter and how to handle them: - - added: create a new CLI command and register it - - modified: update the existing command, its parameters and/or output handling - - removed: delete or deprecate the existing CLI command gracefully - - refactored: update internals only — keep the CLI interface identical - - STYLE REFERENCE (follow this exactly): - {style_ref} - - TEST REFERENCE (follow this pattern): - {test_ref} - - CHANGELOG FORMAT (match this exactly): - {changelog} - - CURRENT VERSION LINE: - {version} - - CURRENT SUPPORTED ENDPOINTS MANIFEST: - {json.dumps(manifest, indent=2)} - - PROJECT STRUCTURE NOTES: - - Commands are implemented as methods in cloudos_cli/clos.py or in - module files under cloudos_cli// (e.g. cloudos_cli/jobs/job.py, - cloudos_cli/datasets/datasets.py, cloudos_cli/procurement/images.py). - There is NO cloudos_cli/commands/ directory — do not create one. - - Version is stored in cloudos_cli/_version.py as: __version__ = 'X.Y.Z' - There is NO pyproject.toml — do not create one. - - Tests live under tests/ and use pytest + responses + mock libraries. - - INSTRUCTIONS: - - Increment the patch version in cloudos_cli/_version.py - - Add a CHANGELOG entry at the top of CHANGELOG.md matching the format above - - Create or modify the relevant module file under cloudos_cli// - - Write at least 2 unit tests in tests/ - - Update supported-endpoints.json to reflect the change - - For removed endpoints: add a deprecation notice in the changelog, remove - the command implementation, and remove the entry from supported-endpoints.json - - Respond with ONLY a JSON object — no explanation, no markdown fences: - {{ - "pr_title": "feat|fix|chore: short description", - "pr_body": "markdown PR description", - "files": [ - {{ - "path": "relative/path/from/repo/root.py", - "action": "create|modify|delete", - "content": "full file content as string (omit for delete)" - }} - ] - }} - """ - - # Testing models.inference.ai.azure.com with gpt-4o - body = json.dumps({ - "model": "gpt-4o", - "max_tokens": 32768, - "messages": [{"role": "user", "content": prompt}] - }).encode() - - req = urllib.request.Request( - "https://models.inference.ai.azure.com/chat/completions", - data=body, - headers={ - "Content-Type": "application/json", - "Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}" - } - ) - try: - with urllib.request.urlopen(req) as r: - result = json.loads(r.read()) - except urllib.error.HTTPError as e: - body_bytes = e.read() - print(f"GitHub Models API error {e.code}: {body_bytes.decode()}", flush=True) - raise - - # OpenAI format: choices[0].message.content - raw_text = result["choices"][0]["message"]["content"].strip() - # Strip markdown code fences if the model wrapped the JSON response - if raw_text.startswith("```"): - raw_text = raw_text.split("\n", 1)[1] # drop the ```json line - raw_text = raw_text.rsplit("```", 1)[0].strip() - response = json.loads(raw_text) - - # Write generated files - for f in response["files"]: - p = pathlib.Path(f["path"]) - if f["action"] == "delete": - p.unlink(missing_ok=True) - else: - p.parent.mkdir(parents=True, exist_ok=True) - p.write_text(f["content"]) - - # Write PR metadata for subsequent steps - open("/tmp/pr-title.txt", "w").write(response["pr_title"]) - open("/tmp/pr-body.md", "w").write(response["pr_body"]) + version = open("cloudos_cli/_version.py").read().strip() + changelog = open("CHANGELOG.md").read()[-1000:] + + # Filter manifest to relevant entries only + changed_prefixes = set() + for ep in endpoints: + parts = ep["path"].strip("/").split("/") + changed_prefixes.add("/".join(parts[:3])) + relevant = [e for e in manifest["endpoints"] if any( + e["path"].strip("/").startswith(p) for p in changed_prefixes + )] or manifest["endpoints"][:5] + + body = f"""## API endpoint change detected + +**Source PR:** {source_pr} + +### Changed endpoints +```json +{json.dumps(endpoints, indent=2)} +``` + +### Change type guide +- `added`: create a new CLI command and register it +- `modified`: update the existing command, parameters and/or output handling +- `removed`: deprecate and delete the existing CLI command gracefully +- `refactored`: update internals only — keep the CLI interface identical + +### What to implement +- Increment the patch version in `cloudos_cli/_version.py` (currently: `{version}`) +- Add a CHANGELOG entry at the top of `CHANGELOG.md` matching the existing format +- Create or modify the relevant module file under `cloudos_cli//` + - Commands live in `cloudos_cli/clos.py` or module files like `cloudos_cli/jobs/job.py`, `cloudos_cli/datasets/datasets.py` + - There is **no** `cloudos_cli/commands/` directory — do not create one +- Write at least 2 unit tests in `tests/` using pytest + responses + mock +- Update `supported-endpoints.json` to reflect the change + +### Relevant existing manifest entries +```json +{json.dumps(relevant, indent=2)} +``` + +### Recent changelog (match this format exactly) +``` +{changelog} +``` +""" + + open("/tmp/issue-body.md", "w").write(body) + ep = endpoints[0] + title = f"{ep['change_type']}: {ep['method']} {ep['path']}" + open("/tmp/issue-title.txt", "w").write(title) EOF - - name: Create branch and commit - id: branch - run: | - # Extract the first endpoint path from the resolved payload (works for all trigger types) - SLUG=$(python3 -c "import json; p=json.load(open('/tmp/payload.json')); print(p['changed_endpoints'][0]['path'])" \ - | tr '/' '-' | tr -d '{}' | sed 's/^-//') - BRANCH="agent/endpoint-$(date +%Y%m%d%H%M%S)-${SLUG}" - git config user.name "cloudos-agent[bot]" - git config user.email "cloudos-agent@users.noreply.github.com" - git checkout -b "$BRANCH" - git add -A - # Only commit if there are actual changes; exit cleanly otherwise - if git diff --cached --quiet; then - echo "No files were changed by the agent — nothing to commit." - exit 1 - fi - git commit -m "$(cat /tmp/pr-title.txt)" - git push origin "$BRANCH" - echo "branch=$BRANCH" >> $GITHUB_OUTPUT - - - name: Open draft PR + - name: Create issue and assign to Copilot coding agent env: GH_TOKEN: ${{ secrets.AGENT_PR_PAT }} run: | - gh pr create \ - --draft \ - --base main \ - --head "${{ steps.branch.outputs.branch }}" \ - --title "$(cat /tmp/pr-title.txt)" \ - --body-file /tmp/pr-body.md \ - --label "agent-generated" + ISSUE_URL=$(gh issue create \ + --title "$(cat /tmp/issue-title.txt)" \ + --body-file /tmp/issue-body.md \ + --label "agent-generated") + ISSUE_NUMBER=$(echo "$ISSUE_URL" | grep -o '[0-9]*$') + echo "Created issue #$ISSUE_NUMBER: $ISSUE_URL" + + # Assigning to @github-copilot triggers the coding agent + gh api repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/assignees \ + -f "assignees[]=github-copilot" + echo "Copilot agent triggered on issue #$ISSUE_NUMBER" - name: Open fallback issue on failure if: failure() @@ -244,6 +161,6 @@ jobs: GH_TOKEN: ${{ secrets.AGENT_PR_PAT }} run: | gh issue create \ - --title "Agent failed: endpoint change requires manual wrapping" \ + --title "Agent workflow failed: could not create Copilot task issue" \ --body "The agent workflow failed. Trigger payload: $(cat /tmp/payload.json)" \ --label "agent-failed" \ No newline at end of file From 59bda3bad951a58e1b8ed75fd04d04c8487affe5 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 18:55:13 +0200 Subject: [PATCH 12/33] chore: remove paths filter so any push to ai-server-cli triggers workflow --- .github/workflows/receive-endpoint-event.yml | 60 ++++++++------------ 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 8bec5eb3..6fe0dd48 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -20,8 +20,6 @@ on: push: branches: - ai-server-cli - paths: - - '.github/test-trigger.json' jobs: generate-pr: @@ -98,40 +96,30 @@ jobs: e["path"].strip("/").startswith(p) for p in changed_prefixes )] or manifest["endpoints"][:5] - body = f"""## API endpoint change detected - -**Source PR:** {source_pr} - -### Changed endpoints -```json -{json.dumps(endpoints, indent=2)} -``` - -### Change type guide -- `added`: create a new CLI command and register it -- `modified`: update the existing command, parameters and/or output handling -- `removed`: deprecate and delete the existing CLI command gracefully -- `refactored`: update internals only — keep the CLI interface identical - -### What to implement -- Increment the patch version in `cloudos_cli/_version.py` (currently: `{version}`) -- Add a CHANGELOG entry at the top of `CHANGELOG.md` matching the existing format -- Create or modify the relevant module file under `cloudos_cli//` - - Commands live in `cloudos_cli/clos.py` or module files like `cloudos_cli/jobs/job.py`, `cloudos_cli/datasets/datasets.py` - - There is **no** `cloudos_cli/commands/` directory — do not create one -- Write at least 2 unit tests in `tests/` using pytest + responses + mock -- Update `supported-endpoints.json` to reflect the change - -### Relevant existing manifest entries -```json -{json.dumps(relevant, indent=2)} -``` - -### Recent changelog (match this format exactly) -``` -{changelog} -``` -""" + fence = "```" + body = ( + "## API endpoint change detected\n\n" + f"**Source PR:** {source_pr}\n\n" + "### Changed endpoints\n" + f"{fence}json\n{json.dumps(endpoints, indent=2)}\n{fence}\n\n" + "### Change type guide\n" + "- `added`: create a new CLI command and register it\n" + "- `modified`: update the existing command, parameters and/or output handling\n" + "- `removed`: deprecate and delete the existing CLI command gracefully\n" + "- `refactored`: update internals only - keep the CLI interface identical\n\n" + "### What to implement\n" + f"- Increment the patch version in `cloudos_cli/_version.py` (currently: `{version}`)\n" + "- Add a CHANGELOG entry at the top of `CHANGELOG.md` matching the existing format\n" + "- Create or modify the relevant module file under `cloudos_cli//`\n" + " - Commands live in `cloudos_cli/clos.py` or module files like `cloudos_cli/jobs/job.py`\n" + " - There is **no** `cloudos_cli/commands/` directory - do not create one\n" + "- Write at least 2 unit tests in `tests/` using pytest + responses + mock\n" + "- Update `supported-endpoints.json` to reflect the change\n\n" + "### Relevant existing manifest entries\n" + f"{fence}json\n{json.dumps(relevant, indent=2)}\n{fence}\n\n" + "### Recent changelog (match this format exactly)\n" + f"{fence}\n{changelog}\n{fence}\n" + ) open("/tmp/issue-body.md", "w").write(body) ep = endpoints[0] From 072ff681350b06556bab509ff49bd29dd1ba477c Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 19:03:59 +0200 Subject: [PATCH 13/33] mock: copilot run --- .github/workflows/receive-endpoint-event.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 6fe0dd48..e91ca773 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -138,9 +138,10 @@ jobs: ISSUE_NUMBER=$(echo "$ISSUE_URL" | grep -o '[0-9]*$') echo "Created issue #$ISSUE_NUMBER: $ISSUE_URL" - # Assigning to @github-copilot triggers the coding agent - gh api repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/assignees \ - -f "assignees[]=github-copilot" + # Trigger the Copilot coding agent by posting a comment mention. + # This is equivalent to clicking "Start coding session" in the GitHub UI. + gh api repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/comments \ + -f "body=@github-copilot Please implement the changes described in this issue." echo "Copilot agent triggered on issue #$ISSUE_NUMBER" - name: Open fallback issue on failure From 4dfc1f4bc763430262d597d2aee7894851384746 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 19:14:28 +0200 Subject: [PATCH 14/33] mock: copilot run --- .github/workflows/receive-endpoint-event.yml | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index e91ca773..e13e7708 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -131,17 +131,16 @@ jobs: env: GH_TOKEN: ${{ secrets.AGENT_PR_PAT }} run: | - ISSUE_URL=$(gh issue create \ + ISSUE_NUMBER=$(gh issue create \ --title "$(cat /tmp/issue-title.txt)" \ --body-file /tmp/issue-body.md \ - --label "agent-generated") - ISSUE_NUMBER=$(echo "$ISSUE_URL" | grep -o '[0-9]*$') - echo "Created issue #$ISSUE_NUMBER: $ISSUE_URL" + --label "agent-generated" \ + --json number \ + --jq '.number') + echo "Created issue #$ISSUE_NUMBER" - # Trigger the Copilot coding agent by posting a comment mention. - # This is equivalent to clicking "Start coding session" in the GitHub UI. - gh api repos/${{ github.repository }}/issues/${ISSUE_NUMBER}/comments \ - -f "body=@github-copilot Please implement the changes described in this issue." + # Assign copilot-swe-agent to trigger the Copilot coding agent + gh issue edit "$ISSUE_NUMBER" --add-assignee copilot-swe-agent echo "Copilot agent triggered on issue #$ISSUE_NUMBER" - name: Open fallback issue on failure From 6caf0e9e0af751488a3edc6b54bb2fadbe648692 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 9 Jun 2026 19:17:01 +0200 Subject: [PATCH 15/33] fix: create issue and assign copilot-swe-agent in single gh command --- .github/workflows/receive-endpoint-event.yml | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index e13e7708..61c94fa1 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -131,17 +131,12 @@ jobs: env: GH_TOKEN: ${{ secrets.AGENT_PR_PAT }} run: | - ISSUE_NUMBER=$(gh issue create \ + gh issue create \ --title "$(cat /tmp/issue-title.txt)" \ --body-file /tmp/issue-body.md \ --label "agent-generated" \ - --json number \ - --jq '.number') - echo "Created issue #$ISSUE_NUMBER" - - # Assign copilot-swe-agent to trigger the Copilot coding agent - gh issue edit "$ISSUE_NUMBER" --add-assignee copilot-swe-agent - echo "Copilot agent triggered on issue #$ISSUE_NUMBER" + --assignee copilot-swe-agent + echo "Issue created and assigned to copilot-swe-agent" - name: Open fallback issue on failure if: failure() From 0efb2784a9d8d7e4aa32cbddf175dc38f2cbdb6f Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 16 Jun 2026 16:33:44 +0200 Subject: [PATCH 16/33] refactor: only accept repository dispatch as trigger --- .github/workflows/receive-endpoint-event.yml | 34 ++++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 61c94fa1..1d54c28b 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -3,23 +3,23 @@ name: Receive endpoint change and generate CLI PR on: repository_dispatch: types: [endpoint-changed] - workflow_dispatch: - inputs: - mock_payload_json: - description: "Full client_payload JSON (paste the mock payload)" - required: false - default: '' - mock_endpoint_path: - description: "Quick mock — endpoint path (e.g. /projects/{id}/members)" - required: false - default: '/projects/{id}/members' - change_type: - description: "added, modified, removed, or refactored" - required: false - default: 'added' - push: - branches: - - ai-server-cli + # workflow_dispatch: + # inputs: + # mock_payload_json: + # description: "Full client_payload JSON (paste the mock payload)" + # required: false + # default: '' + # mock_endpoint_path: + # description: "Quick mock — endpoint path (e.g. /projects/{id}/members)" + # required: false + # default: '/projects/{id}/members' + # change_type: + # description: "added, modified, removed, or refactored" + # required: false + # default: 'added' + # push: + # branches: + # - ai-server-cli jobs: generate-pr: From 85e3e8b52d5b6a7512a0bf258cca270846b01903 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Tue, 16 Jun 2026 17:08:43 +0200 Subject: [PATCH 17/33] refactor: receive workflow dispatch --- .github/workflows/receive-endpoint-event.yml | 51 +++++--------------- 1 file changed, 12 insertions(+), 39 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 1d54c28b..a245ada9 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -1,22 +1,14 @@ name: Receive endpoint change and generate CLI PR on: - repository_dispatch: - types: [endpoint-changed] - # workflow_dispatch: - # inputs: - # mock_payload_json: - # description: "Full client_payload JSON (paste the mock payload)" - # required: false - # default: '' - # mock_endpoint_path: - # description: "Quick mock — endpoint path (e.g. /projects/{id}/members)" - # required: false - # default: '/projects/{id}/members' - # change_type: - # description: "added, modified, removed, or refactored" - # required: false - # default: 'added' + # repository_dispatch: + # types: [endpoint-changed] + workflow_dispatch: + inputs: + client_payload: + description: "Full client_payload JSON (the object under client_payload from the dispatch body)" + required: true + default: '{"relevant": true, "source_pr_url": "", "changed_endpoints": [{"method": "GET", "path": "/test/mock", "change_type": "added", "auth": ["userApiToken"], "summary": "Test dispatch"}]}' # push: # branches: # - ai-server-cli @@ -35,34 +27,15 @@ jobs: env: # Use env vars to safely pass user inputs — avoids single-quote injection # when the JSON payload contains quotes. - MOCK_PAYLOAD_JSON: ${{ inputs.mock_payload_json }} - MOCK_ENDPOINT_PATH: ${{ inputs.mock_endpoint_path }} - CHANGE_TYPE: ${{ inputs.change_type }} + CLIENT_PAYLOAD: ${{ inputs.client_payload }} run: | if [ "${{ github.event_name }}" = "push" ]; then # Sentinel-file trigger: payload is read directly from the committed file cp .github/test-trigger.json /tmp/payload.json elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - if [ -n "$MOCK_PAYLOAD_JSON" ]; then - # Write via env var — safe against single-quote injection - printf '%s' "$MOCK_PAYLOAD_JSON" > /tmp/payload.json - else - python3 -c " - import json, os - payload = { - 'relevant': True, - 'source_pr_url': 'mock', - 'changed_endpoints': [{ - 'method': 'GET', - 'path': os.environ['MOCK_ENDPOINT_PATH'], - 'change_type': os.environ['CHANGE_TYPE'], - 'auth': ['userApiToken'], - 'summary': 'Mock endpoint for testing' - }] - } - print(json.dumps(payload)) - " > /tmp/payload.json - fi + # workflow_dispatch receives the client_payload object as a JSON string + # input — write it via env var (safe against single-quote injection). + printf '%s' "$CLIENT_PAYLOAD" > /tmp/payload.json else # repository_dispatch: client_payload is already JSON — write via tojson filter cat <<'EOJSON' > /tmp/payload.json From fd6ed9917de5d46f579440da544b6fda820b16d7 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Wed, 17 Jun 2026 14:24:23 +0200 Subject: [PATCH 18/33] refactor: add more assignee agents --- .github/workflows/receive-endpoint-event.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index a245ada9..b85cf95a 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -103,13 +103,15 @@ jobs: - name: Create issue and assign to Copilot coding agent env: GH_TOKEN: ${{ secrets.AGENT_PR_PAT }} + ASSIGNEE: anthropic-code-agent run: | + # switch agent: anthropic-code-agent; copilot-swe-agent; codex-code-agent? gh issue create \ --title "$(cat /tmp/issue-title.txt)" \ --body-file /tmp/issue-body.md \ --label "agent-generated" \ - --assignee copilot-swe-agent - echo "Issue created and assigned to copilot-swe-agent" + --assignee $ASSIGNEE + echo "Issue created and assigned to $ASSIGNEE" - name: Open fallback issue on failure if: failure() From 5b579a837a29b3fd853ef128fba145837584870d Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Wed, 17 Jun 2026 15:01:27 +0200 Subject: [PATCH 19/33] ci: improve issue generation from api-server payload --- .github/workflows/receive-endpoint-event.yml | 95 +++++++++++++------- 1 file changed, 64 insertions(+), 31 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index b85cf95a..606c1828 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -46,9 +46,9 @@ jobs: - name: Build Copilot issue body run: | python3 - <<'EOF' - import json, pathlib, textwrap + import json - payload = json.load(open("/tmp/payload.json")) + payload = json.load(open("/tmp/payload.json")) if not payload.get("relevant", True): print("Payload marked as not relevant — skipping.") @@ -56,42 +56,75 @@ jobs: endpoints = payload["changed_endpoints"] source_pr = payload.get("source_pr_url", "manual trigger") - manifest = json.load(open("supported-endpoints.json")) version = open("cloudos_cli/_version.py").read().strip() - changelog = open("CHANGELOG.md").read()[-1000:] - - # Filter manifest to relevant entries only - changed_prefixes = set() - for ep in endpoints: - parts = ep["path"].strip("/").split("/") - changed_prefixes.add("/".join(parts[:3])) - relevant = [e for e in manifest["endpoints"] if any( - e["path"].strip("/").startswith(p) for p in changed_prefixes - )] or manifest["endpoints"][:5] fence = "```" body = ( - "## API endpoint change detected\n\n" + "## Task: implement a CloudOS API endpoint change in the CLI\n\n" + "You are a coding agent working on the **cloudos-cli** Python package. An " + "endpoint changed in the CloudOS API server and the CLI must be brought in " + "sync. Follow the existing codebase conventions exactly — do not invent new " + "patterns or directories.\n\n" f"**Source PR:** {source_pr}\n\n" - "### Changed endpoints\n" + "### Changed endpoint(s)\n" f"{fence}json\n{json.dumps(endpoints, indent=2)}\n{fence}\n\n" "### Change type guide\n" - "- `added`: create a new CLI command and register it\n" - "- `modified`: update the existing command, parameters and/or output handling\n" - "- `removed`: deprecate and delete the existing CLI command gracefully\n" - "- `refactored`: update internals only - keep the CLI interface identical\n\n" - "### What to implement\n" - f"- Increment the patch version in `cloudos_cli/_version.py` (currently: `{version}`)\n" - "- Add a CHANGELOG entry at the top of `CHANGELOG.md` matching the existing format\n" - "- Create or modify the relevant module file under `cloudos_cli//`\n" - " - Commands live in `cloudos_cli/clos.py` or module files like `cloudos_cli/jobs/job.py`\n" - " - There is **no** `cloudos_cli/commands/` directory - do not create one\n" - "- Write at least 2 unit tests in `tests/` using pytest + responses + mock\n" - "- Update `supported-endpoints.json` to reflect the change\n\n" - "### Relevant existing manifest entries\n" - f"{fence}json\n{json.dumps(relevant, indent=2)}\n{fence}\n\n" - "### Recent changelog (match this format exactly)\n" - f"{fence}\n{changelog}\n{fence}\n" + "- `added`: create a new CLI command and register it.\n" + "- `modified`: update the existing command, its parameters and/or output " + "handling.\n" + "- `removed`: deprecate and delete the existing CLI command gracefully.\n" + "- `refactored`: update internals only — keep the CLI interface identical.\n\n" + "### Step 1 — locate the affected area\n" + "- The CLI is **rich-click** based. The root group is defined in " + "`cloudos_cli/__main__.py`, which imports each module's command group from " + "its `cli.py` and registers it via `add_command()`.\n" + "- Each feature lives in its own subdirectory under `cloudos_cli/` " + "(e.g. `jobs/`, `workflows/`, `projects/`, `datasets/`, `queue/`). Inside " + "each module:\n" + " - `cli.py` defines the Click group and command options.\n" + " - `.py` (e.g. `jobs/job.py`) holds the logic class that performs " + "the actual API calls.\n" + "- Map the changed `path` to the right module using " + "`supported-endpoints.json`: each entry has `id`, `method`, `path`, " + "`cli_command` and `source_file`. Use `source_file` to find the exact file " + "to edit. There is **no** `cloudos_cli/commands/` directory — do not create " + "one.\n\n" + "### Step 2 — follow the API-call conventions\n" + "- Logic classes inherit from the `Cloudos` dataclass in " + "`cloudos_cli/clos.py`, which holds `cloudos_url` and `apikey`.\n" + "- Build requests with headers " + "`{\"Content-type\": \"application/json\", \"apikey\": self.apikey}` and the " + "base URL `f\"{self.cloudos_url}/api/v1/...\"` (use the API version the " + "endpoint `path` specifies).\n" + "- Always use the retry-aware wrappers in `cloudos_cli/utils/requests.py` " + "(`retry_requests_get`, `retry_requests_post`, `retry_requests_put`, " + "`retry_requests_delete`) — never call `requests` directly.\n" + "- Raise the shared exceptions from `cloudos_cli/utils/errors.py` " + "(e.g. `BadRequestException`, `NotAuthorisedException`) for error handling.\n" + "- For user-facing output, reuse the table/DataFrame formatting helpers in " + "`cloudos_cli/utils/details.py` rather than printing raw JSON.\n\n" + "### Step 3 — apply the change consistent with the conventions above\n" + "- Edit the module `cli.py` and its logic class to add/modify/remove the " + "command according to the change type, matching the style of neighbouring " + "commands (option naming, docstrings, return shapes).\n" + "- If a new command group is introduced, register it in " + "`cloudos_cli/__main__.py` exactly like the existing ones.\n\n" + "### Step 4 — tests\n" + "- Add or update tests under `tests/`, grouped by module " + "(e.g. `tests/test_jobs/`), file names prefixed with `test_`.\n" + "- Use `pytest`, `responses` (`@responses.activate` to mock the endpoint) " + "and `unittest.mock`, following the pattern in existing tests such as " + "`tests/test_jobs/test_send_job.py`. Write at least 2 unit tests covering " + "the change.\n\n" + "### Step 5 — bookkeeping\n" + f"- Increment the patch version in `cloudos_cli/_version.py` (currently: " + f"`{version}`).\n" + "- Add a new entry at the **top** of `CHANGELOG.md`, keeping the exact " + "existing format: a `## v (YYYY-MM-DD)` heading followed by the " + "relevant category section(s) (`### Feat:`, `### Patch`, `### Breaking:`) " + "with bulleted one-line summaries. Match the most recent entry's style.\n" + "- Update `supported-endpoints.json` so the affected entry reflects the new " + "`method`/`path`/`cli_command`/`source_file` (or add/remove the entry).\n" ) open("/tmp/issue-body.md", "w").write(body) From e30da02d5889f888b77a5b1e17763ad77361c138 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Wed, 17 Jun 2026 16:11:02 +0200 Subject: [PATCH 20/33] refactor: use copilot agent --- .github/workflows/receive-endpoint-event.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 606c1828..3075e213 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -136,7 +136,7 @@ jobs: - name: Create issue and assign to Copilot coding agent env: GH_TOKEN: ${{ secrets.AGENT_PR_PAT }} - ASSIGNEE: anthropic-code-agent + ASSIGNEE: copilot-swe-agent run: | # switch agent: anthropic-code-agent; copilot-swe-agent; codex-code-agent? gh issue create \ From 0c5c4ec5b9a8ed7d39d0e23bbc991430173e1d42 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Wed, 17 Jun 2026 16:32:47 +0200 Subject: [PATCH 21/33] refactor: use grapql to assign agent (now fails with gh --assignee) --- .github/workflows/receive-endpoint-event.yml | 49 +++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 3075e213..3473c95f 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -133,18 +133,55 @@ jobs: open("/tmp/issue-title.txt", "w").write(title) EOF - - name: Create issue and assign to Copilot coding agent + - name: Create issue and assign to the coding agent env: GH_TOKEN: ${{ secrets.AGENT_PR_PAT }} + # The agent's bot login. Must appear in the repo's suggestedActors + # (CAN_BE_ASSIGNED). Options: copilot-swe-agent, anthropic-code-agent, ... ASSIGNEE: copilot-swe-agent + REPO: ${{ github.repository }} run: | - # switch agent: anthropic-code-agent; copilot-swe-agent; codex-code-agent? - gh issue create \ + set -euo pipefail + OWNER="${REPO%/*}" + NAME="${REPO#*/}" + + # 1) Create the issue (no assignee yet) and capture its GraphQL node id. + ISSUE_URL=$(gh issue create \ --title "$(cat /tmp/issue-title.txt)" \ --body-file /tmp/issue-body.md \ - --label "agent-generated" \ - --assignee $ASSIGNEE - echo "Issue created and assigned to $ASSIGNEE" + --label "agent-generated") + echo "Created issue: $ISSUE_URL" + ISSUE_NUMBER="${ISSUE_URL##*/}" + ISSUE_ID=$(gh api "repos/$OWNER/$NAME/issues/$ISSUE_NUMBER" --jq '.node_id') + + # 2) Resolve the agent's bot node id from suggestedActors. REST + # --assignee attaches the bot but does NOT start an agent session; + # the GraphQL replaceActorsForAssignable mutation (what the UI uses) + # is what actually bootstraps the run. + ACTOR_ID=$(gh api graphql -f query=' + query($owner:String!,$name:String!){ + repository(owner:$owner,name:$name){ + suggestedActors(capabilities:[CAN_BE_ASSIGNED], first:50){ + nodes{ login __typename ... on Bot { id } ... on User { id } } + } + } + }' -F owner="$OWNER" -F name="$NAME" \ + --jq ".data.repository.suggestedActors.nodes[] | select(.login==\"$ASSIGNEE\") | .id") + + if [ -z "$ACTOR_ID" ]; then + echo "::error::Agent '$ASSIGNEE' not found in suggestedActors (CAN_BE_ASSIGNED)." + exit 1 + fi + + # 3) Assign the agent via GraphQL — this triggers the agent session. + gh api graphql -f query=' + mutation($assignable:ID!,$actor:ID!){ + replaceActorsForAssignable(input:{assignableId:$assignable, actorIds:[$actor]}){ + assignable { ... on Issue { number } } + } + }' -F assignable="$ISSUE_ID" -F actor="$ACTOR_ID" + + echo "Issue #$ISSUE_NUMBER assigned to $ASSIGNEE via GraphQL" - name: Open fallback issue on failure if: failure() From 4bc47f7de0ca97db0b3f7c7307f9d589424a761d Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Wed, 17 Jun 2026 17:20:03 +0200 Subject: [PATCH 22/33] debug: add latency between issue generation and assignment --- .github/workflows/receive-endpoint-event.yml | 48 ++++---------------- 1 file changed, 10 insertions(+), 38 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 3473c95f..9d6f1024 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -133,55 +133,27 @@ jobs: open("/tmp/issue-title.txt", "w").write(title) EOF - - name: Create issue and assign to the coding agent + - name: Create issue and assign to Copilot coding agent env: GH_TOKEN: ${{ secrets.AGENT_PR_PAT }} - # The agent's bot login. Must appear in the repo's suggestedActors - # (CAN_BE_ASSIGNED). Options: copilot-swe-agent, anthropic-code-agent, ... ASSIGNEE: copilot-swe-agent - REPO: ${{ github.repository }} run: | - set -euo pipefail - OWNER="${REPO%/*}" - NAME="${REPO#*/}" - - # 1) Create the issue (no assignee yet) and capture its GraphQL node id. + # switch agent: anthropic-code-agent; copilot-swe-agent; codex-code-agent? + # Create the issue first WITHOUT an assignee, then assign separately. ISSUE_URL=$(gh issue create \ --title "$(cat /tmp/issue-title.txt)" \ --body-file /tmp/issue-body.md \ --label "agent-generated") echo "Created issue: $ISSUE_URL" - ISSUE_NUMBER="${ISSUE_URL##*/}" - ISSUE_ID=$(gh api "repos/$OWNER/$NAME/issues/$ISSUE_NUMBER" --jq '.node_id') - - # 2) Resolve the agent's bot node id from suggestedActors. REST - # --assignee attaches the bot but does NOT start an agent session; - # the GraphQL replaceActorsForAssignable mutation (what the UI uses) - # is what actually bootstraps the run. - ACTOR_ID=$(gh api graphql -f query=' - query($owner:String!,$name:String!){ - repository(owner:$owner,name:$name){ - suggestedActors(capabilities:[CAN_BE_ASSIGNED], first:50){ - nodes{ login __typename ... on Bot { id } ... on User { id } } - } - } - }' -F owner="$OWNER" -F name="$NAME" \ - --jq ".data.repository.suggestedActors.nodes[] | select(.login==\"$ASSIGNEE\") | .id") - - if [ -z "$ACTOR_ID" ]; then - echo "::error::Agent '$ASSIGNEE' not found in suggestedActors (CAN_BE_ASSIGNED)." - exit 1 - fi + ISSUE_NUMBER=$(basename "$ISSUE_URL") - # 3) Assign the agent via GraphQL — this triggers the agent session. - gh api graphql -f query=' - mutation($assignable:ID!,$actor:ID!){ - replaceActorsForAssignable(input:{assignableId:$assignable, actorIds:[$actor]}){ - assignable { ... on Issue { number } } - } - }' -F assignable="$ISSUE_ID" -F actor="$ACTOR_ID" + # Give GitHub a moment to fully index the new issue before assigning the + # coding agent — assigning too quickly can fail to start the agent session. + sleep 30 - echo "Issue #$ISSUE_NUMBER assigned to $ASSIGNEE via GraphQL" + gh issue edit "$ISSUE_NUMBER" \ + --add-assignee "$ASSIGNEE" + echo "Issue #$ISSUE_NUMBER assigned to $ASSIGNEE" - name: Open fallback issue on failure if: failure() From 3420c1f5f428241250365115d03cebea1391fd1b Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Wed, 17 Jun 2026 17:34:48 +0200 Subject: [PATCH 23/33] debug: lower issue body --- .github/workflows/receive-endpoint-event.yml | 69 ++------------------ 1 file changed, 7 insertions(+), 62 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 9d6f1024..06ae2409 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -60,71 +60,16 @@ jobs: fence = "```" body = ( - "## Task: implement a CloudOS API endpoint change in the CLI\n\n" - "You are a coding agent working on the **cloudos-cli** Python package. An " - "endpoint changed in the CloudOS API server and the CLI must be brought in " - "sync. Follow the existing codebase conventions exactly — do not invent new " - "patterns or directories.\n\n" + "## Sync CLI with a CloudOS API endpoint change\n\n" f"**Source PR:** {source_pr}\n\n" "### Changed endpoint(s)\n" f"{fence}json\n{json.dumps(endpoints, indent=2)}\n{fence}\n\n" - "### Change type guide\n" - "- `added`: create a new CLI command and register it.\n" - "- `modified`: update the existing command, its parameters and/or output " - "handling.\n" - "- `removed`: deprecate and delete the existing CLI command gracefully.\n" - "- `refactored`: update internals only — keep the CLI interface identical.\n\n" - "### Step 1 — locate the affected area\n" - "- The CLI is **rich-click** based. The root group is defined in " - "`cloudos_cli/__main__.py`, which imports each module's command group from " - "its `cli.py` and registers it via `add_command()`.\n" - "- Each feature lives in its own subdirectory under `cloudos_cli/` " - "(e.g. `jobs/`, `workflows/`, `projects/`, `datasets/`, `queue/`). Inside " - "each module:\n" - " - `cli.py` defines the Click group and command options.\n" - " - `.py` (e.g. `jobs/job.py`) holds the logic class that performs " - "the actual API calls.\n" - "- Map the changed `path` to the right module using " - "`supported-endpoints.json`: each entry has `id`, `method`, `path`, " - "`cli_command` and `source_file`. Use `source_file` to find the exact file " - "to edit. There is **no** `cloudos_cli/commands/` directory — do not create " - "one.\n\n" - "### Step 2 — follow the API-call conventions\n" - "- Logic classes inherit from the `Cloudos` dataclass in " - "`cloudos_cli/clos.py`, which holds `cloudos_url` and `apikey`.\n" - "- Build requests with headers " - "`{\"Content-type\": \"application/json\", \"apikey\": self.apikey}` and the " - "base URL `f\"{self.cloudos_url}/api/v1/...\"` (use the API version the " - "endpoint `path` specifies).\n" - "- Always use the retry-aware wrappers in `cloudos_cli/utils/requests.py` " - "(`retry_requests_get`, `retry_requests_post`, `retry_requests_put`, " - "`retry_requests_delete`) — never call `requests` directly.\n" - "- Raise the shared exceptions from `cloudos_cli/utils/errors.py` " - "(e.g. `BadRequestException`, `NotAuthorisedException`) for error handling.\n" - "- For user-facing output, reuse the table/DataFrame formatting helpers in " - "`cloudos_cli/utils/details.py` rather than printing raw JSON.\n\n" - "### Step 3 — apply the change consistent with the conventions above\n" - "- Edit the module `cli.py` and its logic class to add/modify/remove the " - "command according to the change type, matching the style of neighbouring " - "commands (option naming, docstrings, return shapes).\n" - "- If a new command group is introduced, register it in " - "`cloudos_cli/__main__.py` exactly like the existing ones.\n\n" - "### Step 4 — tests\n" - "- Add or update tests under `tests/`, grouped by module " - "(e.g. `tests/test_jobs/`), file names prefixed with `test_`.\n" - "- Use `pytest`, `responses` (`@responses.activate` to mock the endpoint) " - "and `unittest.mock`, following the pattern in existing tests such as " - "`tests/test_jobs/test_send_job.py`. Write at least 2 unit tests covering " - "the change.\n\n" - "### Step 5 — bookkeeping\n" - f"- Increment the patch version in `cloudos_cli/_version.py` (currently: " - f"`{version}`).\n" - "- Add a new entry at the **top** of `CHANGELOG.md`, keeping the exact " - "existing format: a `## v (YYYY-MM-DD)` heading followed by the " - "relevant category section(s) (`### Feat:`, `### Patch`, `### Breaking:`) " - "with bulleted one-line summaries. Match the most recent entry's style.\n" - "- Update `supported-endpoints.json` so the affected entry reflects the new " - "`method`/`path`/`cli_command`/`source_file` (or add/remove the entry).\n" + "### Steps\n" + "1. Find the file via `source_file` in `supported-endpoints.json`; CLI groups register in `cloudos_cli/__main__.py`, each module has `cli.py` (commands) + `.py` (logic class extending `Cloudos` in `cloudos_cli/clos.py`).\n" + "2. Apply the change per `change_type` (added/modified/removed/refactored), using `cloudos_cli/utils/requests.py` wrappers, `apikey` header, and matching neighbouring command style.\n" + "3. Add ≥2 tests under `tests//` with pytest + `responses` (`@responses.activate`).\n" + f"4. Bump patch version in `cloudos_cli/_version.py` (now `{version}`) and add a top `CHANGELOG.md` entry in the existing format.\n" + "5. Update `supported-endpoints.json` to reflect the change.\n" ) open("/tmp/issue-body.md", "w").write(body) From eea9384620ef6a7cec9c0a3fac53ac0fa823e7b5 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Wed, 17 Jun 2026 17:42:21 +0200 Subject: [PATCH 24/33] debug: unassign and assign agent --- .github/workflows/receive-endpoint-event.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 06ae2409..c0bbb29c 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -96,8 +96,18 @@ jobs: # coding agent — assigning too quickly can fail to start the agent session. sleep 30 - gh issue edit "$ISSUE_NUMBER" \ - --add-assignee "$ASSIGNEE" + # Retry: unassign then re-assign to nudge the agent session into starting. + for i in 1 2 3; do + gh issue edit "$ISSUE_NUMBER" \ + --remove-assignee "$ASSIGNEE" || true + + sleep 5 + + gh issue edit "$ISSUE_NUMBER" \ + --add-assignee "$ASSIGNEE" && break + + sleep 30 + done echo "Issue #$ISSUE_NUMBER assigned to $ASSIGNEE" - name: Open fallback issue on failure From 6cb2f021041d4c3abfe9d79230d4f0e8f37aad4e Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Thu, 18 Jun 2026 12:20:35 +0200 Subject: [PATCH 25/33] refactor: open directly PR --- .github/workflows/receive-endpoint-event.yml | 61 ++++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index c0bbb29c..354e638f 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -43,7 +43,7 @@ jobs: EOJSON fi - - name: Build Copilot issue body + - name: Build Copilot PR body run: | python3 - <<'EOF' import json @@ -72,43 +72,42 @@ jobs: "5. Update `supported-endpoints.json` to reflect the change.\n" ) - open("/tmp/issue-body.md", "w").write(body) + open("/tmp/pr-body.md", "w").write(body) ep = endpoints[0] title = f"{ep['change_type']}: {ep['method']} {ep['path']}" - open("/tmp/issue-title.txt", "w").write(title) + open("/tmp/pr-title.txt", "w").write(title) EOF - - name: Create issue and assign to Copilot coding agent + - name: Start Copilot cloud agent task (create session) env: + # Agent tasks API requires a USER-to-server token (classic PAT / OAuth / + # GitHub App user token). Server-to-server (GITHUB_TOKEN / App installation + # tokens) are NOT supported. GH_TOKEN: ${{ secrets.AGENT_PR_PAT }} - ASSIGNEE: copilot-swe-agent + REPO: ${{ github.repository }} + AGENT_MODEL: claude-sonnet-4.6 run: | - # switch agent: anthropic-code-agent; copilot-swe-agent; codex-code-agent? - # Create the issue first WITHOUT an assignee, then assign separately. - ISSUE_URL=$(gh issue create \ - --title "$(cat /tmp/issue-title.txt)" \ - --body-file /tmp/issue-body.md \ - --label "agent-generated") - echo "Created issue: $ISSUE_URL" - ISSUE_NUMBER=$(basename "$ISSUE_URL") - - # Give GitHub a moment to fully index the new issue before assigning the - # coding agent — assigning too quickly can fail to start the agent session. - sleep 30 - - # Retry: unassign then re-assign to nudge the agent session into starting. - for i in 1 2 3; do - gh issue edit "$ISSUE_NUMBER" \ - --remove-assignee "$ASSIGNEE" || true - - sleep 5 - - gh issue edit "$ISSUE_NUMBER" \ - --add-assignee "$ASSIGNEE" && break - - sleep 30 - done - echo "Issue #$ISSUE_NUMBER assigned to $ASSIGNEE" + set -euo pipefail + gh auth status + + # Native cloud-agent session: POST /agents/repos/{owner}/{repo}/tasks. + # This starts the agent directly instead of assigning an issue, so it + # doesn't depend on the issue-assignment session bootstrap that was failing. + # Include "model" only when AGENT_MODEL is set (empty => auto selection). + jq -n \ + --rawfile prompt /tmp/pr-body.md \ + --arg base "main" \ + --arg model "${AGENT_MODEL:-}" \ + '{prompt: $prompt, base_ref: $base, create_pull_request: true} + + (if $model == "" then {} else {model: $model} end)' \ + | gh api \ + --method POST \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "/agents/repos/$REPO/tasks" \ + --input - + + echo "Cloud agent task created for $REPO (model: ${AGENT_MODEL:-auto})" - name: Open fallback issue on failure if: failure() From 27bfcb796fb17374fbe4a906a12d5b2a4108141c Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Thu, 18 Jun 2026 12:59:40 +0200 Subject: [PATCH 26/33] refactor: make payload and prompt more detailed --- .github/workflows/receive-endpoint-event.yml | 131 ++++++++++++++++--- 1 file changed, 114 insertions(+), 17 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 354e638f..96a8cbd4 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -8,7 +8,7 @@ on: client_payload: description: "Full client_payload JSON (the object under client_payload from the dispatch body)" required: true - default: '{"relevant": true, "source_pr_url": "", "changed_endpoints": [{"method": "GET", "path": "/test/mock", "change_type": "added", "auth": ["userApiToken"], "summary": "Test dispatch"}]}' + default: '{"relevant": true, "source_pr_url": "", "commit": "", "summary": "1 supported endpoint(s) affected: GET /api/v1/analytics/team", "changed_files": ["src/api/analytics/router.ts"], "changed_endpoints": [{"method": "GET", "path": "/api/v1/analytics/team", "change_type": "modified", "auth": ["userApiToken"], "summary": "team analytics", "source": "typescript", "api_server_files": ["src/api/analytics/router.ts"], "id": "analytics-team", "cli_command": "cloudos analytics team", "cli_source_file": "cloudos_cli/analytics/analytics.py", "query_params": ["teamId"]}]}' # push: # branches: # - ai-server-cli @@ -47,34 +47,131 @@ jobs: run: | python3 - <<'EOF' import json + import sys payload = json.load(open("/tmp/payload.json")) if not payload.get("relevant", True): print("Payload marked as not relevant — skipping.") - import sys; sys.exit(0) + sys.exit(0) - endpoints = payload["changed_endpoints"] - source_pr = payload.get("source_pr_url", "manual trigger") - version = open("cloudos_cli/_version.py").read().strip() + endpoints = payload.get("changed_endpoints", []) + if not endpoints: + print("No changed endpoints in payload — skipping.") + sys.exit(0) + + source_pr = payload.get("source_pr_url") or "manual trigger" + commit = payload.get("commit", "") + overall = payload.get("summary", "") + changed_files = payload.get("changed_files", []) + version = open("cloudos_cli/_version.py").read().strip() fence = "```" - body = ( - "## Sync CLI with a CloudOS API endpoint change\n\n" - f"**Source PR:** {source_pr}\n\n" - "### Changed endpoint(s)\n" - f"{fence}json\n{json.dumps(endpoints, indent=2)}\n{fence}\n\n" - "### Steps\n" - "1. Find the file via `source_file` in `supported-endpoints.json`; CLI groups register in `cloudos_cli/__main__.py`, each module has `cli.py` (commands) + `.py` (logic class extending `Cloudos` in `cloudos_cli/clos.py`).\n" - "2. Apply the change per `change_type` (added/modified/removed/refactored), using `cloudos_cli/utils/requests.py` wrappers, `apikey` header, and matching neighbouring command style.\n" - "3. Add ≥2 tests under `tests//` with pytest + `responses` (`@responses.activate`).\n" - f"4. Bump patch version in `cloudos_cli/_version.py` (now `{version}`) and add a top `CHANGELOG.md` entry in the existing format.\n" - "5. Update `supported-endpoints.json` to reflect the change.\n" + + def fmt_list(items): + return ", ".join(items) if items else "—" + + # Group endpoints by the CLI source file they map to, so the agent can + # tackle one module at a time. + by_file = {} + for ep in endpoints: + by_file.setdefault(ep.get("cli_source_file") or "(unknown)", []).append(ep) + + lines = [] + lines.append("## Sync cloudos-cli with a CloudOS API endpoint change\n") + lines.append(f"**Source PR:** {source_pr} ") + if commit: + lines.append(f"**api-server commit:** `{commit}` ") + lines.append(f"**Summary:** {overall}\n") + + lines.append("### What changed (and where to act)\n") + lines.append( + "Each endpoint below was detected as changed in the api-server. " + "`source` tells you whether the change was found in the OpenAPI " + "`specification.yaml` (`specification`), in the TypeScript route/handler " + "code (`typescript`), or both. A `typescript`-only change means the " + "public contract may have shifted WITHOUT a spec update — read the " + "linked api-server files carefully before assuming the request/response " + "shape.\n" ) + for ep in endpoints: + method = ep.get("method") or "" + path = ep.get("path") or "" + change = ep.get("change_type") or "modified" + header = f"#### `{change}` — {method} {path}".strip() + lines.append(header) + if ep.get("new_path"): + lines.append(f"- **Renamed/new path:** `{ep['new_path']}`") + if ep.get("id"): + lines.append(f"- **Endpoint id:** `{ep['id']}` (key in `supported-endpoints.json`)") + if ep.get("cli_command"): + lines.append(f"- **CLI command to update:** `{ep['cli_command']}`") + if ep.get("cli_source_file"): + lines.append(f"- **CLI source file:** `{ep['cli_source_file']}`") + if ep.get("query_params"): + lines.append(f"- **Query params:** {fmt_list(ep['query_params'])}") + if ep.get("auth"): + lines.append(f"- **Auth strategies:** {fmt_list(ep['auth'])}") + lines.append(f"- **Detection source:** {ep.get('source', 'specification')}") + if ep.get("api_server_files"): + lines.append( + "- **api-server files that changed:** " + + fmt_list([f"`{f}`" for f in ep["api_server_files"]]) + ) + if ep.get("summary"): + lines.append(f"- **Notes:** {ep['summary']}") + lines.append("") + + if changed_files: + lines.append("
All changed api-server files\n") + for f in changed_files: + lines.append(f"- `{f}`") + lines.append("\n
\n") + + lines.append("### Full machine-readable payload\n") + lines.append(f"{fence}json\n{json.dumps(endpoints, indent=2)}\n{fence}\n") + + lines.append("### How cloudos-cli is structured\n") + lines.append( + "- CLI command groups register in `cloudos_cli/__main__.py`.\n" + "- Each module under `cloudos_cli//` has a `cli.py` (Click " + "commands / options) plus a logic class (e.g. `.py`) that " + "usually extends `Cloudos` in `cloudos_cli/clos.py`.\n" + "- All HTTP calls go through the wrappers in " + "`cloudos_cli/utils/requests.py` and send the `apikey` header.\n" + "- `supported-endpoints.json` is the source of truth mapping each " + "endpoint to its `cli_command` and `source_file`.\n" + ) + + lines.append("### Required steps\n") + lines.append( + "1. For every endpoint above, open its `cli_source_file` (fall back to " + "looking it up by `id` in `supported-endpoints.json` if missing) and " + "the api-server files listed, to understand the exact contract change.\n" + "2. Apply the change according to `change_type`:\n" + " - `added`: add the new command/option and wrapper call.\n" + " - `modified`: update params, request body, response parsing, and " + "any affected Click options to match the new contract.\n" + " - `removed`: deprecate/remove the wrapping command and its tests.\n" + " - renamed (`new_path` present): update the request path/URL builder.\n" + "3. Route all HTTP changes through `cloudos_cli/utils/requests.py`, keep " + "the `apikey` header, and match the style of neighbouring commands.\n" + "4. Add or update at least 2 tests under `tests//` using pytest " + "and `responses` (`@responses.activate`), covering happy path and one " + "failure/edge case.\n" + f"5. Bump the patch version in `cloudos_cli/_version.py` (currently " + f"`{version}`) and add a top `CHANGELOG.md` entry in the existing format.\n" + "6. Update `supported-endpoints.json` so the mapping reflects the new " + "path/params/method (and bump its `cli_version` if you change behaviour).\n" + ) + + body = "\n".join(lines) open("/tmp/pr-body.md", "w").write(body) + ep = endpoints[0] - title = f"{ep['change_type']}: {ep['method']} {ep['path']}" + extra = f" (+{len(endpoints) - 1} more)" if len(endpoints) > 1 else "" + title = f"{ep.get('change_type', 'update')}: {ep.get('method', '')} {ep.get('path', '')}{extra}".strip() open("/tmp/pr-title.txt", "w").write(title) EOF From 36732c355e37cf979e9663d6babd3e180c3711a4 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Thu, 18 Jun 2026 13:04:30 +0200 Subject: [PATCH 27/33] fix: update issue for failed PR --- .github/workflows/receive-endpoint-event.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 96a8cbd4..fb7d17b2 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -212,6 +212,6 @@ jobs: GH_TOKEN: ${{ secrets.AGENT_PR_PAT }} run: | gh issue create \ - --title "Agent workflow failed: could not create Copilot task issue" \ + --title "Agent workflow failed: could not create Copilot task PR" \ --body "The agent workflow failed. Trigger payload: $(cat /tmp/payload.json)" \ --label "agent-failed" \ No newline at end of file From 8eacfeba8a58143f7f7f9b8fcefc45551216da87 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Fri, 19 Jun 2026 15:20:50 +0200 Subject: [PATCH 28/33] test: adding a new endpoint support --- supported-endpoints.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/supported-endpoints.json b/supported-endpoints.json index f221f0f5..4d9808f2 100644 --- a/supported-endpoints.json +++ b/supported-endpoints.json @@ -638,6 +638,16 @@ "source_file": "cloudos_cli/clos.py", "added_in_version": "2.91.0", "notes": "Called internally to poll folder deletion status during 'cloudos job workdir' and dataset operations" + }, + { + "id": "analytics-team-summary", + "method": "GET", + "path": "/api/v1/analytics/team/summary", + "query_params": ["teamId", "startDate", "endDate", "granularity"], + "cli_command": "cloudos analytics team-summary", + "source_file": "cloudos_cli/analytics/analytics.py", + "added_in_version": "2.92.0", + "notes": "Returns aggregated team usage analytics (compute hours, job counts, spend) over a date range. Made-up endpoint added to exercise the endpoint-sync automation." } ] } From bd726d8d26955f4c75e4cfc9e19a96294d825dcc Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Fri, 19 Jun 2026 16:57:10 +0200 Subject: [PATCH 29/33] refactor: remove test trigger --- .github/test-trigger.json | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .github/test-trigger.json diff --git a/.github/test-trigger.json b/.github/test-trigger.json deleted file mode 100644 index 60943cb6..00000000 --- a/.github/test-trigger.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "relevant": true, - "source_pr_url": "mock", - "changed_endpoints": [ - { - "method": "GET", - "path": "/api/v1/projects/{id}/members", - "change_type": "added", - "auth": ["userApiToken"], - "summary": "Returns members of a project" - } - ] -} From faecea0e4fb267a1de2699460c928b67909179de Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Fri, 19 Jun 2026 16:57:23 +0200 Subject: [PATCH 30/33] docs: update version --- cloudos_cli/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudos_cli/_version.py b/cloudos_cli/_version.py index 1271f796..51e998d4 100644 --- a/cloudos_cli/_version.py +++ b/cloudos_cli/_version.py @@ -1 +1 @@ -__version__ = '2.91.0' +__version__ = '2.94.0' From fa39e665fed1fc2bc3a0183b147500856d4e9327 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Fri, 19 Jun 2026 17:00:04 +0200 Subject: [PATCH 31/33] docs: update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07f2df07..6c96664f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ## lifebit-ai/cloudos-cli: changelog +## v2.94.0 (2026-06-19) + +### Feat: + +- Adds GitHub workflow to receive api-server endpoint events and open Copilot agent PRs +- Adds `supported-endpoints.json` mapping wrapped API endpoints to CLI commands + ## v2.91.0 (2026-05-28) ### Feat: From d15ee9774aa7c61810e122a84c8bbaee693254db Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Fri, 19 Jun 2026 17:09:47 +0200 Subject: [PATCH 32/33] ci: remove unused triggers --- .github/workflows/receive-endpoint-event.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index fb7d17b2..83f8916b 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -1,17 +1,12 @@ name: Receive endpoint change and generate CLI PR on: - # repository_dispatch: - # types: [endpoint-changed] workflow_dispatch: inputs: client_payload: description: "Full client_payload JSON (the object under client_payload from the dispatch body)" required: true default: '{"relevant": true, "source_pr_url": "", "commit": "", "summary": "1 supported endpoint(s) affected: GET /api/v1/analytics/team", "changed_files": ["src/api/analytics/router.ts"], "changed_endpoints": [{"method": "GET", "path": "/api/v1/analytics/team", "change_type": "modified", "auth": ["userApiToken"], "summary": "team analytics", "source": "typescript", "api_server_files": ["src/api/analytics/router.ts"], "id": "analytics-team", "cli_command": "cloudos analytics team", "cli_source_file": "cloudos_cli/analytics/analytics.py", "query_params": ["teamId"]}]}' - # push: - # branches: - # - ai-server-cli jobs: generate-pr: From 33e00a4fa2b8c82551df1d3a8cb7b8d3ac9e7cd0 Mon Sep 17 00:00:00 2001 From: Daniel Boloc Date: Mon, 22 Jun 2026 12:05:54 +0200 Subject: [PATCH 33/33] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/receive-endpoint-event.yml | 25 +++++++++++++------- supported-endpoints.json | 14 ++--------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/.github/workflows/receive-endpoint-event.yml b/.github/workflows/receive-endpoint-event.yml index 83f8916b..6ddaa13b 100644 --- a/.github/workflows/receive-endpoint-event.yml +++ b/.github/workflows/receive-endpoint-event.yml @@ -8,6 +8,9 @@ on: required: true default: '{"relevant": true, "source_pr_url": "", "commit": "", "summary": "1 supported endpoint(s) affected: GET /api/v1/analytics/team", "changed_files": ["src/api/analytics/router.ts"], "changed_endpoints": [{"method": "GET", "path": "/api/v1/analytics/team", "change_type": "modified", "auth": ["userApiToken"], "summary": "team analytics", "source": "typescript", "api_server_files": ["src/api/analytics/router.ts"], "id": "analytics-team", "cli_command": "cloudos analytics team", "cli_source_file": "cloudos_cli/analytics/analytics.py", "query_params": ["teamId"]}]}' +permissions: + contents: read + jobs: generate-pr: runs-on: ubuntu-latest @@ -24,10 +27,7 @@ jobs: # when the JSON payload contains quotes. CLIENT_PAYLOAD: ${{ inputs.client_payload }} run: | - if [ "${{ github.event_name }}" = "push" ]; then - # Sentinel-file trigger: payload is read directly from the committed file - cp .github/test-trigger.json /tmp/payload.json - elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then # workflow_dispatch receives the client_payload object as a JSON string # input — write it via env var (safe against single-quote injection). printf '%s' "$CLIENT_PAYLOAD" > /tmp/payload.json @@ -164,10 +164,7 @@ jobs: body = "\n".join(lines) open("/tmp/pr-body.md", "w").write(body) - ep = endpoints[0] - extra = f" (+{len(endpoints) - 1} more)" if len(endpoints) > 1 else "" - title = f"{ep.get('change_type', 'update')}: {ep.get('method', '')} {ep.get('path', '')}{extra}".strip() - open("/tmp/pr-title.txt", "w").write(title) + # NOTE: PR title generation removed because it is not used by any subsequent step. EOF - name: Start Copilot cloud agent task (create session) @@ -206,7 +203,17 @@ jobs: env: GH_TOKEN: ${{ secrets.AGENT_PR_PAT }} run: | + { + echo "The agent workflow failed." + echo + echo "Trigger payload:" + echo '```json' + cat /tmp/payload.json + echo + echo '```' + } > /tmp/failure-body.md + gh issue create \ --title "Agent workflow failed: could not create Copilot task PR" \ - --body "The agent workflow failed. Trigger payload: $(cat /tmp/payload.json)" \ + --body-file /tmp/failure-body.md \ --label "agent-failed" \ No newline at end of file diff --git a/supported-endpoints.json b/supported-endpoints.json index 4d9808f2..f7a586c3 100644 --- a/supported-endpoints.json +++ b/supported-endpoints.json @@ -1,12 +1,12 @@ { "version": "1.0", - "cli_version": "2.91.0", + "cli_version": "2.94.0", "description": "Single source of truth mapping every API endpoint wrapped by cloudos-cli to the CLI command that wraps it. Used by the AI agent PR workflow to detect breaking changes in the api-server repo.", "notes": [ "Path parameters are expressed as {param_name}.", "Query parameters (teamId, page, limit, etc.) are omitted from 'path'; they appear in 'query_params'.", "Entries with cli_command starting with '_internal/' are not directly user-facing but are called as helpers during a user command.", - "added_in_version reflects the CLI release when this mapping was first formally tracked (2.91.0). Historical endpoint additions were not back-filled." + "added_in_version reflects the CLI release when this mapping entry was first formally tracked. Historical endpoint additions were not back-filled." ], "endpoints": [ { @@ -638,16 +638,6 @@ "source_file": "cloudos_cli/clos.py", "added_in_version": "2.91.0", "notes": "Called internally to poll folder deletion status during 'cloudos job workdir' and dataset operations" - }, - { - "id": "analytics-team-summary", - "method": "GET", - "path": "/api/v1/analytics/team/summary", - "query_params": ["teamId", "startDate", "endDate", "granularity"], - "cli_command": "cloudos analytics team-summary", - "source_file": "cloudos_cli/analytics/analytics.py", - "added_in_version": "2.92.0", - "notes": "Returns aggregated team usage analytics (compute hours, job counts, spend) over a date range. Made-up endpoint added to exercise the endpoint-sync automation." } ] }