diff --git a/.x/coordinator.md b/.x/coordinator.md index fbf5a5489ea..c69869e48d7 100644 --- a/.x/coordinator.md +++ b/.x/coordinator.md @@ -16,8 +16,13 @@ For this repository, apply the generic loop priorities as follows: `Azure/azure-cli` before normal work. Never act on a dispute from another repository. 2. Handle explicit, deduplicated human feedback on an Agent-managed PR. -3. Promote completed Copilot fork work and complete any required AAZ source - promotion before downstream readiness. +3. Promote completed Copilot fork work. After a downstream CLI PR exists, use + the repository-owned `start_aaz_source_task` custom skill when generated + AAZ output requires a durable source change. Discover completed source work + with `find_aaz_fork_prs_ready_for_promotion`, promote it with + `promote_aaz_fork_pr`, and confirm the live source PR with + `find_promoted_aaz_source_pr` before downstream readiness. Do not invoke the + neutral generation-source bridge primitives directly. 4. Trigger missing CI for a ready fork PR. 5. Send an actionable in-flight PR to Tester, then Reviewer after required live tests and CI complete. diff --git a/.x/fixer.md b/.x/fixer.md index 48edaf607be..819b09e8bd0 100644 --- a/.x/fixer.md +++ b/.x/fixer.md @@ -19,15 +19,18 @@ for a due single follow-up. Stop after either write. ## Target and implementation routing -For sufficient reports, call `infer_target_for_repo` with the sanitized text. -Verify the returned target against current repository structure. +For sufficient reports, call the repository-owned `infer_target_for_repo` +custom skill with `repo_full_name="Azure/azure-cli"`, the sanitized text, and +an empty `pr_files` list. It resolves only against the configured live module +and extension roots. Verify the returned target against current repository +structure. - A core module remains in `Azure/azure-cli`. Build the exact `[Component] Fix #N: \`az ...\`: Summary` title with `pr_title_for`, include `pr_format_guidance`, post the evidence-based bug analysis, then start the configured Copilot fork task. - An extension is routed with the idempotent - `start_extension_tracker_task` workflow to + repository-owned `start_extension_tracker_task` custom skill to `Azure/azure-cli-extensions`. It creates or resumes the tracker, records a pending source marker, starts Copilot in the extension fork, and finalizes the source backlink only after dispatch succeeds. Include the complete diff --git a/.x/reviewer.md b/.x/reviewer.md index 6e7ab513efd..6f6e9022d1a 100644 --- a/.x/reviewer.md +++ b/.x/reviewer.md @@ -10,9 +10,9 @@ human reviews, and live-test state once. Pending required CI or live tests are waiting, not failure. If the current decisive human review requests changes, preserve that state and do not post an Agent pass. -Run `get_pr_regression_coverage_summary` and `get_pr_review_skill_summary` -against -the current diff. Deterministic findings are requirements. Semantic candidates +Run the repository-owned `get_pr_regression_coverage_summary` custom skill +with the PR number, then run `get_pr_review_skill_summary` against the current +diff. Deterministic findings are requirements. Semantic candidates become findings only when changed-line evidence confirms them. Diagnose each failed check as PR-related, unrelated, or uncertain and include the exact evidence, practical correction, and focused verification. @@ -28,9 +28,12 @@ Require: - owning-team review for high-risk auth, security, core runtime, generated surface, or broad behavior changes. -Use `repair_pr_title_check` only for a confirmed metadata-gate failure, -then read the rerun in a later round. Combine CI, live-test, regression, risk, -and review-skill evidence in one review. +Use `repair_pr_title_check` only for a confirmed metadata-gate failure. Resolve +the component first with repository-owned `infer_target_for_repo` using the +current PR title, body, and changed filenames, then pass its name as +`component`; central title repair must not infer repository policy. Read the +rerun in a later round. Combine CI, live-test, regression, risk, and +review-skill evidence in one review. For a human-requested PR, post one `COMMENT`. For a Copilot-authored PR with relevant failures, use `request_copilot_changes`; after the iteration cap, diff --git a/.x/skills/README.md b/.x/skills/README.md deleted file mode 100644 index e2327c98c91..00000000000 --- a/.x/skills/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Custom skills - -This repository currently uses the approved X Engineering Agent base skill -library. A repository-owned custom skill must be one Python file containing -one public top-level function and must be mapped in `.x/x.yml`. Markdown files -in this directory are documentation and are never executable. diff --git a/.x/skills/changed_test_files.py b/.x/skills/changed_test_files.py new file mode 100644 index 00000000000..658beecea10 --- /dev/null +++ b/.x/skills/changed_test_files.py @@ -0,0 +1,27 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +"""Select changed Azure CLI live-test files.""" + + +def changed_test_files(pr_files): + """Return unique changed pytest paths outside azure-cli-core.""" + selected = [] + seen = set() + for path in pr_files or []: + normalized = str(path).replace("\\", "/") + lowered = normalized.casefold() + name = normalized.rsplit("/", 1)[-1] + if ( + "/tests/" not in f"/{lowered}" + or not name.casefold().startswith("test_") + or not name.casefold().endswith(".py") + or "azure-cli-core" in lowered.split("/") + ): + continue + if normalized not in seen: + seen.add(normalized) + selected.append(normalized) + return selected diff --git a/.x/skills/find_aaz_fork_prs_ready_for_promotion.py b/.x/skills/find_aaz_fork_prs_ready_for_promotion.py new file mode 100644 index 00000000000..14775a85685 --- /dev/null +++ b/.x/skills/find_aaz_fork_prs_ready_for_promotion.py @@ -0,0 +1,13 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +"""Bind generation-source candidate discovery to Azure CLI.""" + + +def find_aaz_fork_prs_ready_for_promotion(): + """Find completed AAZ fork pull requests ready for promotion.""" + return find_generation_source_fork_prs_ready_for_promotion( + repository="Azure/azure-cli", + ) diff --git a/.x/skills/find_promoted_aaz_source_pr.py b/.x/skills/find_promoted_aaz_source_pr.py new file mode 100644 index 00000000000..5fbe0c22296 --- /dev/null +++ b/.x/skills/find_promoted_aaz_source_pr.py @@ -0,0 +1,14 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +"""Bind promoted generation-source lookup to Azure CLI.""" + + +def find_promoted_aaz_source_pr(issue_number): + """Find the promoted AAZ source pull request for an Agent issue.""" + return find_promoted_generation_source_pr( + repository="Azure/azure-cli", + issue_number=issue_number, + ) diff --git a/.x/skills/get_pr_regression_coverage_summary.py b/.x/skills/get_pr_regression_coverage_summary.py new file mode 100644 index 00000000000..6f2827bab5d --- /dev/null +++ b/.x/skills/get_pr_regression_coverage_summary.py @@ -0,0 +1,71 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +"""Evaluate Azure CLI command-module regression coverage.""" + + +def get_pr_regression_coverage_summary(pr_number): + """Find changed command modules without focused tests or recordings.""" + changes = get_pr_file_changes( + owner="Azure", + repo="azure-cli", + pr_number=pr_number, + ) + files = [ + item.get("filename") + for item in changes + if isinstance(item, dict) and item.get("filename") + ] + root = "src/azure-cli/azure/cli/command_modules/" + production_files = [] + modules = set() + for path in files: + normalized = str(path).replace("\\", "/") + name = normalized.rsplit("/", 1)[-1] + if ( + normalized.startswith(root) + and normalized.endswith(".py") + and "/tests/" not in normalized + and name not in {"__init__.py", "_help.py"} + ): + production_files.append(normalized) + remainder = normalized[len(root):] + module = remainder.split("/", 1)[0].split(".", 1)[0].casefold() + if module: + modules.add(module) + + test_files = [] + recording_files = [] + covered = set() + for path in files: + normalized = str(path).replace("\\", "/") + if not normalized.startswith(root) or "/tests/" not in normalized: + continue + module = ( + normalized[len(root):] + .split("/", 1)[0] + .split(".", 1)[0] + .casefold() + ) + if module not in modules: + continue + name = normalized.rsplit("/", 1)[-1] + if name.casefold().startswith("test_") and name.casefold().endswith(".py"): + test_files.append(normalized) + covered.add(module) + if "/recordings/" in normalized: + recording_files.append(normalized) + covered.add(module) + + uncovered = sorted(modules - covered) + return { + "applicable": bool(production_files), + "gap": bool(uncovered), + "modules": sorted(modules), + "uncovered_modules": uncovered, + "production_files": production_files, + "test_files": test_files, + "recording_files": recording_files, + } diff --git a/.x/skills/infer_target_for_repo.py b/.x/skills/infer_target_for_repo.py new file mode 100644 index 00000000000..50287c81c58 --- /dev/null +++ b/.x/skills/infer_target_for_repo.py @@ -0,0 +1,85 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +"""Infer an Azure CLI module or extension from trusted repository structure.""" + + +def infer_target_for_repo(repo_full_name, text, pr_files): + """Resolve a sanitized issue or PR diff to a live CLI target.""" + if repo_full_name != "Azure/azure-cli": + raise ValueError("infer_target_for_repo is restricted to Azure/azure-cli") + + modules = list_repository_directories( + source_repository="Azure/azure-cli", + ) + extensions = list_repository_directories( + source_repository="Azure/azure-cli-extensions", + ) + + def normalize(value): + return "".join( + character + for character in str(value or "").casefold() + if character.isalnum() + ) + + def resolve(candidate): + candidate_normalized = normalize(candidate) + for extension in extensions: + if normalize(extension) == candidate_normalized: + return { + "kind": "extension", + "name": extension, + "repo": "Azure/azure-cli-extensions", + } + for module in modules: + if normalize(module) == candidate_normalized: + return { + "kind": "module", + "name": module, + "repo": "Azure/azure-cli", + } + return None + + scores = {} + for path in pr_files or []: + parts = str(path).replace("\\", "/").split("/") + if "command_modules" in parts: + index = parts.index("command_modules") + if index + 1 < len(parts): + candidate = parts[index + 1] + scores[candidate] = scores.get(candidate, 0) + 10 + elif len(parts) > 1 and parts[0].casefold() == "src": + candidate = parts[1] + scores[candidate] = scores.get(candidate, 0) + 10 + if pr_files: + for candidate in sorted(scores, key=lambda item: (-scores[item], item)): + target = resolve(candidate) + if target is not None: + return target + return {"kind": "none", "name": None, "repo": None} + + cleaned = "".join( + character if character.isalnum() or character in "-_./" else " " + for character in str(text or "").casefold() + ) + words = cleaned.split() + for index, word in enumerate(words): + if word == "az" and index + 1 < len(words): + candidate = words[index + 1] + scores[candidate] = scores.get(candidate, 0) + 5 + if word.startswith("src/"): + parts = word.split("/") + if len(parts) > 1: + candidate = parts[1] + scores[candidate] = scores.get(candidate, 0) + 3 + if "command_modules/" in word: + candidate = word.split("command_modules/", 1)[1].split("/", 1)[0] + scores[candidate] = scores.get(candidate, 0) + 3 + for candidate in sorted(scores, key=lambda item: (-scores[item], item)): + target = resolve(candidate) + if target is not None: + return target + return {"kind": "unknown", "name": None, "repo": None} diff --git a/.x/skills/promote_aaz_fork_pr.py b/.x/skills/promote_aaz_fork_pr.py new file mode 100644 index 00000000000..d1ecbc94a10 --- /dev/null +++ b/.x/skills/promote_aaz_fork_pr.py @@ -0,0 +1,16 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +"""Bind generation-source promotion to Azure CLI.""" + + +def promote_aaz_fork_pr(fork_pr_number, title, body): + """Promote one validated AAZ fork pull request.""" + return promote_generation_source_fork_pr( + repository="Azure/azure-cli", + fork_pr_number=fork_pr_number, + title=title, + body=body, + ) diff --git a/.x/skills/start_aaz_source_task.py b/.x/skills/start_aaz_source_task.py new file mode 100644 index 00000000000..daf321c2759 --- /dev/null +++ b/.x/skills/start_aaz_source_task.py @@ -0,0 +1,17 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +"""Bind durable generation-source task creation to Azure CLI.""" + + +def start_aaz_source_task(issue_number, downstream_pr_url, changed_files, prompt_context): + """Start the configured AAZ source task for an Azure CLI pull request.""" + return start_generation_source_task( + repository="Azure/azure-cli", + issue_number=issue_number, + downstream_pr_url=downstream_pr_url, + changed_files=changed_files, + prompt_context=prompt_context, + ) diff --git a/.x/skills/start_extension_tracker_task.py b/.x/skills/start_extension_tracker_task.py new file mode 100644 index 00000000000..8b942c2e992 --- /dev/null +++ b/.x/skills/start_extension_tracker_task.py @@ -0,0 +1,27 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +"""Own the Azure CLI to CLI Extensions implementation handoff.""" + + +def start_extension_tracker_task(issue_number, view, target, prompt, command, summary): + """Create or resume the scoped extension tracker and Copilot task.""" + if ( + not isinstance(target, dict) + or target.get("repo") != "Azure/azure-cli-extensions" + or not target.get("name") + ): + raise ValueError( + "start_extension_tracker_task requires a CLI Extensions target" + ) + return start_repository_handoff_task( + repository="Azure/azure-cli", + issue_number=issue_number, + view=view, + target=target, + prompt=prompt, + command=command, + summary=summary, + ) diff --git a/.x/tester.md b/.x/tester.md index dff21ffe389..2344b69d8d8 100644 --- a/.x/tester.md +++ b/.x/tester.md @@ -5,10 +5,20 @@ Coordinator whose current head either has a completed Copilot task marker or is a verified human-requested review candidate, and has no completed live-test run for that head. -Use `dispatch_live_test_workflow` with the PR number and -`pr_repo="Azure/azure-cli"`. Do not provide a guessed module; the dispatcher -resolves changed files against the live module list and the workflow validates -the target. +Read the PR and `get_pr_file_changes` once. Pass the filenames to the +repository-owned `changed_test_files` custom skill and call +`infer_target_for_repo` with `repo_full_name="Azure/azure-cli"`, `text` set to +the PR title/body, and `pr_files` set to those filenames. Use +`dispatch_live_test_workflow` with the PR number, +`pr_repo="Azure/azure-cli"`, the resolved module and target kind, and +`test_files` set to the paths returned by `changed_test_files`. Never guess a +module or test path; repository custom skills own both decisions and the +workflow validates them against the current PR. + +If no test path is selected, call the dispatcher with the empty list so it +records a neutral skip for the current revision. If tests are selected but +target inference does not return a named `module` or `extension`, stop with a +pending result and do not dispatch. Before dispatch, reuse any queued, in-progress, or completed run for the same head SHA. A new dispatch counts as one action; a reused run is a read. Call diff --git a/.x/x.yml b/.x/x.yml index b9eb8e4d5f4..afc0a60f9b1 100644 --- a/.x/x.yml +++ b/.x/x.yml @@ -6,18 +6,17 @@ agents: skills: - auto_trigger_pr_validation - build_promoted_pr_body - - changed_test_files - codegen_execution_guidance - compare_issue_similarity - copilot_iteration_cap_reached - copilot_iteration_state - daily_pr_cap_reached - dispatch_live_test_workflow - - find_aaz_fork_prs_ready_for_promotion - find_copilot_fork_prs_ready_for_promotion - find_fork_prs_needing_ci + - find_generation_source_fork_prs_ready_for_promotion - find_in_flight_prs - - find_promoted_aaz_source_pr + - find_promoted_generation_source_pr - find_sensitive_redaction_dispute - find_stale_prs - follow_up_requirements @@ -30,13 +29,12 @@ skills: - get_pr_check_runs - get_pr_check_summary - get_pr_file_changes - - get_pr_regression_coverage_summary - get_pr_review_skill_summary - get_profile - get_workflow_run - handle_sensitive_redaction_dispute - has_agent_reviewed_head - - infer_target_for_repo + - list_repository_directories - mark_pr_ready_for_review - post_bug_analysis - post_comment @@ -44,7 +42,7 @@ skills: - post_pr_review - pr_format_guidance - pr_title_for - - promote_aaz_fork_pr + - promote_generation_source_fork_pr - promote_copilot_fork_pr - recall_repository_memory - remediate_sensitive_issue @@ -60,10 +58,18 @@ skills: - safe_issue_view - select_triagable_issues_for_repo - similar_issue_candidates - - start_aaz_source_task - start_copilot_fork_task - - start_extension_tracker_task + - start_generation_source_task + - start_repository_handoff_task - synchronize_pull_request_feedback - synchronize_repository_feedback - update_pr_branch -custom_skills: {} +custom_skills: + changed_test_files: changed_test_files + find_aaz_fork_prs_ready_for_promotion: find_aaz_fork_prs_ready_for_promotion + find_promoted_aaz_source_pr: find_promoted_aaz_source_pr + get_pr_regression_coverage_summary: get_pr_regression_coverage_summary + infer_target_for_repo: infer_target_for_repo + promote_aaz_fork_pr: promote_aaz_fork_pr + start_aaz_source_task: start_aaz_source_task + start_extension_tracker_task: start_extension_tracker_task