From bbd70d0d5485a5fdce7c7f0089825c5ff0c7a833 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Mon, 6 Jul 2026 18:27:20 +0200 Subject: [PATCH 01/59] Refactor home AI resource sync skill and update related scripts - Removed deprecated sync_home_ai_resources.py script and updated references to the new run.sh script. - Modified run.sh to handle compact output mode and added quiet mode functionality. - Updated SKILL.md to reflect changes in command usage and output formats. - Enhanced sync output functions to support compact JSON output for better integration with AI tools. - Added tests for compact output and bisync plan functionality to ensure correctness. - Adjusted agent prompts and reporting contracts to emphasize compact output as the default. - Updated inventory and YAML configurations to remove references to obsolete scripts. --- .github/INVENTORY.md | 2 - .github/scripts/run.sh | 6 +- .github/scripts/sync_home_ai_resources.py | 52 ---- .../SKILL.md | 29 ++- .../agents/openai.yaml | 2 +- .../references/sync-contract.md | 20 +- .../scripts/bisync_skills.py | 24 +- .../scripts/run.sh | 45 +++- .../scripts/sync_home_ai_resources.py | 39 ++- .../scripts/sync_output.py | 120 ++++++--- tests/test_home_ai_sync_output.py | 57 +++++ ...ent_sync_install_ai_resources_contracts.py | 233 ++++++++++++++++++ tests/test_run_sh_dispatch.py | 6 +- 13 files changed, 507 insertions(+), 128 deletions(-) delete mode 100755 .github/scripts/sync_home_ai_resources.py mode change 100644 => 100755 .github/skills/local-agent-sync-install-ai-resources/scripts/run.sh create mode 100644 tests/test_home_ai_sync_output.py create mode 100644 tests/test_local_agent_sync_install_ai_resources_contracts.py diff --git a/.github/INVENTORY.md b/.github/INVENTORY.md index 7c16be2a..6689f847 100644 --- a/.github/INVENTORY.md +++ b/.github/INVENTORY.md @@ -84,7 +84,6 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/internal-excel/SKILL.md` - `.github/skills/internal-gateway-critical-master/SKILL.md` - `.github/skills/internal-gateway-execute-plans/SKILL.md` -- `.github/skills/internal-gateway-idea-brainstorming/SKILL.md` - `.github/skills/internal-gateway-idea/SKILL.md` - `.github/skills/internal-gateway-simple-task/SKILL.md` - `.github/skills/internal-gateway-writing-plans/SKILL.md` @@ -173,7 +172,6 @@ These imported `openai-*` office skills remain support-only depth for repositori - `.github/scripts/lib/token_risks.py` - `.github/scripts/run.sh` - `.github/scripts/sync_copilot_catalog.py` -- `.github/scripts/sync_home_ai_resources.py` - `.github/scripts/validate_internal_skills.py` ## Agents diff --git a/.github/scripts/run.sh b/.github/scripts/run.sh index 257f1f78..a7a126e4 100755 --- a/.github/scripts/run.sh +++ b/.github/scripts/run.sh @@ -138,7 +138,7 @@ resolve_script() { printf '%s\n' "$SCRIPT_DIR/detect_token_risks.py" ;; sync_home_ai_resources|sync_home_ai_resources.py) - printf '%s\n' "$SCRIPT_DIR/sync_home_ai_resources.py" + printf '%s\n' "$REPO_ROOT/.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh" ;; sync_copilot_catalog|sync_copilot_catalog.py) printf '%s\n' "$SCRIPT_DIR/sync_copilot_catalog.py" @@ -206,6 +206,10 @@ main() { } shift + if [[ "$script_path" == *.sh ]]; then + exec bash "$script_path" "$@" + fi + load_required_python_version select_python_bin require_command "$PYTHON_BIN" diff --git a/.github/scripts/sync_home_ai_resources.py b/.github/scripts/sync_home_ai_resources.py deleted file mode 100755 index 7ca83aea..00000000 --- a/.github/scripts/sync_home_ai_resources.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 -"""Purpose: repo-local wrapper for the bundled home AI resource sync skill script. - -Usage examples: - python3 ./.github/scripts/sync_home_ai_resources.py sync --targets skills --format report - python3 ./.github/scripts/sync_home_ai_resources.py apply --targets codex --create-missing-dirs --format report -""" - -from __future__ import annotations - -import importlib.util -import sys -from pathlib import Path - -SKILL_SCRIPT_DIR = ( - Path(__file__).resolve().parents[1] - / "skills/local-agent-sync-install-ai-resources/scripts" -) -SKILL_CLI_PATH = SKILL_SCRIPT_DIR / "sync_home_ai_resources.py" - - -def load_skill_cli(): - inserted_path = False - if SKILL_SCRIPT_DIR.as_posix() not in sys.path: - sys.path.insert(0, SKILL_SCRIPT_DIR.as_posix()) - inserted_path = True - try: - spec = importlib.util.spec_from_file_location( - "_local_agent_sync_home_ai_resources_cli", - SKILL_CLI_PATH, - ) - if spec is None or spec.loader is None: - raise RuntimeError(f"Unable to load skill CLI from {SKILL_CLI_PATH}") - module = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = module - spec.loader.exec_module(module) - return module - finally: - if inserted_path: - sys.path.remove(SKILL_SCRIPT_DIR.as_posix()) - - -SKILL_CLI = load_skill_cli() -parse_args = SKILL_CLI.parse_args - - -def main() -> int: - return SKILL_CLI.run(parse_args()) - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/.github/skills/local-agent-sync-install-ai-resources/SKILL.md b/.github/skills/local-agent-sync-install-ai-resources/SKILL.md index 8d0852e7..a15a2ba2 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/SKILL.md +++ b/.github/skills/local-agent-sync-install-ai-resources/SKILL.md @@ -14,7 +14,10 @@ The paired agent is only a thin UX wrapper; this skill owns mode selection, approval posture, safety gates, and report interpretation for repo-to-home sync and bisync. Keep user-visible output deterministic, bounded, and summary-first. -Canonical command examples use `python3 ./.github/scripts/sync_home_ai_resources.py --format report`. +Canonical command examples use `.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh`. +The CLI defaults to `--format compact` for AI/tool iteration. Use +`--format report` only when a human-readable command report is explicitly +needed; in chat, summarize compact output as concise Markdown for the user. ## When to use @@ -39,13 +42,14 @@ Every mode has exactly one command. Do not infer the mode, do not skip blockers, | User request | Lane | Command | | --- | --- | --- | -| Generic `sync`, `repo→home`, or `repo wins` | Auto-run safe repo-to-home install for `skills`, then review only home-owned or ambiguous bisync drift | `python3 ./.github/scripts/sync_home_ai_resources.py sync --targets skills --home-root ~ --format report` | -| Explicit `home→repo` | Review home-newer drift, then use explicit bisync commands with a git-clean repo | `python3 ./.github/scripts/sync_home_ai_resources.py bisync plan --home-root ~ --format report` then `python3 ./.github/scripts/sync_home_ai_resources.py bisync apply --home-root ~ --format report` | -| Readiness check | Verify roots, support matrix, catalog, and state root without writes | `python3 ./.github/scripts/sync_home_ai_resources.py doctor --targets skills --home-root ~ --format report` | -| Dry install review | Show repo-to-home changes without writes | `python3 ./.github/scripts/sync_home_ai_resources.py plan --targets skills --home-root ~ --format report` | -| Explicit install write | Materialize a reviewed install plan | `python3 ./.github/scripts/sync_home_ai_resources.py apply --targets skills --home-root ~ --format report` | +| Generic `sync`, `repo→home`, or `repo wins` | Auto-run safe repo-to-home install for `skills`, then review only home-owned or ambiguous bisync drift | `.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh sync --targets skills` | +| Explicit `home→repo` | Review home-newer drift, then use explicit bisync commands with a git-clean repo | `.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh bisync plan` then `.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh bisync apply` | +| Readiness check | Verify roots, support matrix, catalog, and state root without writes | `.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh doctor --targets skills` | +| Dry install review | Show repo-to-home changes without writes | `.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh plan --targets skills` | +| Explicit install write | Materialize a reviewed install plan | `.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh apply --targets skills` | -For bundle direct-copy, replace `./.github/scripts/sync_home_ai_resources.py` with `./scripts/run.sh`, keep `--format report` on model-facing runs, and omit `--home-root` because it defaults to `$HOME`. +The repository dispatcher `./.github/scripts/run.sh sync_home_ai_resources ...` +may be used for compatibility; it delegates to the bundled skill runner. ### Mode Selection @@ -112,7 +116,10 @@ The `bisync` lane provides explicit bidirectional synchronization between `.gith ## Reporting Contract -Use `--format report` for model-facing runs. Do not dump raw JSON unless the user explicitly asks for it. Machine-readable output remains available as `--format compact` or `--format json` for automation and debugging. +Use compact output for model-facing runs. Do not dump raw JSON unless the user +explicitly asks for it. Machine-readable output is available as `--format compact` +or `--compact` by default; full raw state remains available as `--format json` +for debugging. Use `--format report` only for a human-readable command report. Every report must be summary-first and start with one status line that includes mode or lane, selected targets, overall status, blocker count, and `next_action.action`. @@ -127,8 +134,9 @@ Never report blocker codes alone. Translate each code into a plain-language reas ## Bundled Automation -- Prefer `python3 ./.github/scripts/sync_home_ai_resources.py` for deterministic `plan`, `audit`, `doctor`, `apply`, and `bisync plan|apply` behavior, and keep `--format report` on model-facing runs. -- Use `scripts/run.sh` when a portable skill-local environment is needed; it installs the locked `PyYAML` dependency from `scripts/requirements.txt`. +- Prefer `.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh` for deterministic `plan`, `audit`, `doctor`, `apply`, and `bisync plan|apply` behavior. +- Use compact output for automation and AI loops. Convert compact results into concise Markdown in chat when the user needs to decide what to do. +- The bundled runner installs locked dependencies from `scripts/requirements.txt` and suppresses bootstrap noise when compact output is selected. - Keep orchestration inside `scripts/sync_home_ai_resources.py`, install behavior inside `scripts/home_syncing.py`, bisync behavior inside `scripts/bisync_skills.py`, report rendering inside `scripts/sync_output.py`, and reference loading inside `scripts/home_sync_contract.py`. ## Conflict Resolution @@ -157,5 +165,6 @@ After any manual cleanup, re-run `plan` or `bisync plan` and require zero blocke - Rebuild `.github/INVENTORY.md` when the bundle or related scripts change by using `./.github/scripts/run.sh build_inventory --root .`. - Run `./.github/scripts/run.sh check_catalog_consistency --root . --include-token-risks` after bundle or automation changes. - Run `bash -n .github/skills/local-agent-sync-install-ai-resources/scripts/run.sh .github/scripts/run.sh` after shell entrypoint changes. +- Run `.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh plan --targets skills,copilot,codex --compact` after output or CLI changes to confirm low-token output remains valid. - Run focused agent or skill contract tests for this bundle. - Run focused sync tests for report layout, target parsing, support-matrix policy, manifest handling, overwrite gates, bisync protocol, and missing-directory behavior when automation changes. diff --git a/.github/skills/local-agent-sync-install-ai-resources/agents/openai.yaml b/.github/skills/local-agent-sync-install-ai-resources/agents/openai.yaml index 01f917ad..4abb450b 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/agents/openai.yaml +++ b/.github/skills/local-agent-sync-install-ai-resources/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Home AI Resource Sync" short_description: "Plan, apply, audit, doctor, or bisync local AI home sync" - default_prompt: "Use $local-agent-sync-install-ai-resources to run safe repo-to-home sync with --format report by default; require explicit approval for apply or bisync apply." + default_prompt: "Use $local-agent-sync-install-ai-resources to run safe repo-to-home sync with compact output by default; require explicit approval for apply or bisync apply, and summarize compact results in chat Markdown for user decisions." diff --git a/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md b/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md index a0d25a8e..c2497078 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md +++ b/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md @@ -66,6 +66,13 @@ Each `managed_resources[]` item should capture: ## Reporting Contract +`--format compact` is the default and the preferred format for AI/tool +iteration. It emits a single-line JSON object with status, counts, blockers, +next action, and a small bounded evidence sample. Use `--format report` only +when a human-readable command report is explicitly needed. In chat, agents +should translate compact output into concise Markdown instead of asking the CLI +to spend tokens on tables. + Deterministic report output (`--format report`) and JSON reporting should expose at least: - `selected_targets` @@ -84,7 +91,7 @@ Deterministic report output (`--format report`) and JSON reporting should expose Text reports must use a summary-first layout rather than a raw field dump. Use tables where columns clarify changes, blockers, or completed actions; use short bullets for counts and state summaries. -Model-facing report tables should stay bounded for routine change and completed-action rows. Keep all blocker, attention, and readiness-failure rows visible. When rows are omitted, add an explicit omitted-count row and point to `--format json` for full detail. +Human-readable report tables should stay bounded for routine change and completed-action rows. Keep all blocker, attention, and readiness-failure rows visible. When rows are omitted, add an explicit omitted-count row and point to `--format json` for full detail. ### Shared Header @@ -195,16 +202,17 @@ When no changes are proposed or applied, report `no-op` explicitly with the reas ## Automation Entry Points -- Bundled CLI: `scripts/sync_home_ai_resources.py` -- Bundled dependency bootstrap: `scripts/run.sh` +- Bundled dependency bootstrap and canonical CLI: `scripts/run.sh` +- Bundled Python CLI: `scripts/sync_home_ai_resources.py` - Bundled implementation: `scripts/home_syncing.py` - Bundled bisync engine: `scripts/bisync_skills.py` - Bundled reference loader: `scripts/home_sync_contract.py` - Bundled dependency lock: `scripts/requirements.txt` -- Repository wrapper: `.github/scripts/sync_home_ai_resources.py` -- Repository Bash wrapper: `.github/scripts/sync_home_ai_resources.sh` +- Repository dispatcher compatibility path: `.github/scripts/run.sh sync_home_ai_resources ...` -Prefer the bundled scripts when the skill is direct-copied into a home runtime. Prefer the repository wrappers when running from this source repository because they reuse the repository maintenance-tool environment. +Prefer the bundled runner in this repository and after direct-copy into a home +runtime. The repository dispatcher exists only as a compatibility path and must +delegate to the bundled runner instead of carrying duplicate sync logic. ## Install Sync Contract diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/bisync_skills.py b/.github/skills/local-agent-sync-install-ai-resources/scripts/bisync_skills.py index b9d6715c..2f2cf6cc 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/bisync_skills.py +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/bisync_skills.py @@ -21,7 +21,11 @@ from home_sync_contract import load_home_sync_policy from home_syncing import reconcile_manifest_entry_after_bisync_copy -from sync_output import build_compact_bisync_output, render_bisync_report +from sync_output import ( + build_compact_bisync_output, + dump_compact_json, + render_bisync_report, +) IGNORED_SYNC_PARTS: tuple[str, ...] = (".venv", "__pycache__", ".pytest_cache") IGNORED_SYNC_SUFFIXES: tuple[str, ...] = (".pyc", ".pyo") @@ -565,7 +569,7 @@ def _resolve_source_root(args: argparse.Namespace) -> Path: def _emit_bisync_output(plan: BisyncPlan, format_name: str) -> None: payload = plan.to_dict() if format_name == "compact": - print(json.dumps(build_compact_bisync_output(payload), indent=2, sort_keys=True)) + print(dump_compact_json(build_compact_bisync_output(payload))) return if format_name == "json": print(json.dumps(payload, indent=2, sort_keys=True)) @@ -588,9 +592,14 @@ def build_bisync_parser() -> argparse.ArgumentParser: plan_parser.add_argument( "--format", choices=["text", "json", "compact", "report"], - default="report", + default="compact", help="Output format.", ) + plan_parser.add_argument( + "--compact", + action="store_true", + help="Alias for --format compact; optimized for AI/tool iteration.", + ) apply_parser = subparsers.add_parser("apply", help="Apply bisync resolution") apply_parser.add_argument("--source-root", default=".", help="Source repository root.") apply_parser.add_argument( @@ -601,15 +610,22 @@ def build_bisync_parser() -> argparse.ArgumentParser: apply_parser.add_argument( "--format", choices=["text", "json", "compact", "report"], - default="report", + default="compact", help="Output format.", ) + apply_parser.add_argument( + "--compact", + action="store_true", + help="Alias for --format compact; optimized for AI/tool iteration.", + ) return parser def main(argv: list[str] | None = None) -> None: parser = build_bisync_parser() args = parser.parse_args(argv) + if getattr(args, "compact", False): + args.format = "compact" if args.bisync_command == "plan": raise SystemExit(run_bisync_plan(args)) elif args.bisync_command == "apply": diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh b/.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh old mode 100644 new mode 100755 index e2a00694..78f0dc26 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh @@ -2,16 +2,22 @@ # # Purpose: Bootstrap this skill's local Python environment and run the home AI resource sync tool. # Usage examples: -# ./scripts/run.sh sync --targets skills --format report -# ./scripts/run.sh apply --targets codex --create-missing-dirs --format report +# ./scripts/run.sh sync --targets skills +# ./scripts/run.sh apply --targets codex --create-missing-dirs set -Eeuo pipefail log_info() { + if [[ "${SYNC_HOME_AI_RESOURCES_QUIET:-false}" == "true" ]]; then + return + fi printf 'ℹ️ %s\n' "$*" } log_success() { + if [[ "${SYNC_HOME_AI_RESOURCES_QUIET:-false}" == "true" ]]; then + return + fi printf '✅ %s\n' "$*" } @@ -62,12 +68,45 @@ install_dependencies() { fi log_info "Installing locked skill dependencies." - "$VENV_DIR/bin/pip" install --require-hashes -r "$REQUIREMENTS_FILE" + if [[ "${SYNC_HOME_AI_RESOURCES_QUIET:-false}" == "true" ]]; then + "$VENV_DIR/bin/pip" install --quiet --disable-pip-version-check --require-hashes -r "$REQUIREMENTS_FILE" + else + "$VENV_DIR/bin/pip" install --disable-pip-version-check --require-hashes -r "$REQUIREMENTS_FILE" + fi printf '%s' "$requirements_hash" >"$REQUIREMENTS_HASH_FILE" log_success "Skill Python environment is ready." } +should_quiet() { + local previous="" + for arg in "$@"; do + if [[ "$previous" == "--format" ]]; then + [[ "$arg" == "compact" ]] && return 0 + return 1 + fi + case "$arg" in + --compact|--format=compact) + return 0 + ;; + --format) + previous="--format" + continue + ;; + --format=*) + return 1 + ;; + esac + previous="" + done + return 0 +} + main() { + if should_quiet "$@"; then + export SYNC_HOME_AI_RESOURCES_QUIET="true" + else + export SYNC_HOME_AI_RESOURCES_QUIET="false" + fi require_command "$PYTHON_BIN" ensure_venv install_dependencies diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_home_ai_resources.py b/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_home_ai_resources.py index 9d6c1fee..3525e26b 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_home_ai_resources.py +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_home_ai_resources.py @@ -2,9 +2,9 @@ """Purpose: plan and apply local home-directory AI resource sync operations. Usage examples: - python3 scripts/sync_home_ai_resources.py sync --targets skills --format report - python3 scripts/sync_home_ai_resources.py plan --targets skills --format report - python3 scripts/sync_home_ai_resources.py apply --targets skills --create-missing-dirs --format report + python3 scripts/sync_home_ai_resources.py sync --targets skills + python3 scripts/sync_home_ai_resources.py plan --targets skills --compact + python3 scripts/sync_home_ai_resources.py apply --targets skills --create-missing-dirs """ from __future__ import annotations @@ -30,6 +30,7 @@ from sync_output import ( build_compact_bisync_output, build_compact_install_output, + dump_compact_json, render_doctor_report, render_install_report, render_sync_report, @@ -80,9 +81,14 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: cmd_parser.add_argument( "--format", choices=["text", "json", "compact", "report"], - default="report", + default="compact", help="Output format.", ) + cmd_parser.add_argument( + "--compact", + action="store_true", + help="Alias for --format compact; optimized for AI/tool iteration.", + ) cmd_parser.add_argument( "--fast", action="store_true", @@ -106,9 +112,14 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: bisync_plan.add_argument( "--format", choices=["text", "json", "compact", "report"], - default="report", + default="compact", help="Output format.", ) + bisync_plan.add_argument( + "--compact", + action="store_true", + help="Alias for --format compact; optimized for AI/tool iteration.", + ) bisync_apply = bisync_sub.add_parser("apply", help="Apply bisync resolution.") bisync_apply.add_argument("--source-root", default=".", help="Source repository root.") bisync_apply.add_argument( @@ -119,11 +130,19 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: bisync_apply.add_argument( "--format", choices=["text", "json", "compact", "report"], - default="report", + default="compact", help="Output format.", ) + bisync_apply.add_argument( + "--compact", + action="store_true", + help="Alias for --format compact; optimized for AI/tool iteration.", + ) - return parser.parse_args(argv) + args = parser.parse_args(argv) + if getattr(args, "compact", False): + args.format = "compact" + return args def main(argv: list[str] | None = None) -> int: @@ -370,7 +389,7 @@ def emit_sync_output(payload: dict[str, object], *, format_name: str) -> None: compact["install"] = build_compact_install_output(install_payload) if isinstance(bisync_payload, dict): compact["bisync"] = build_compact_bisync_output(bisync_payload) - print(json.dumps(compact, indent=2, sort_keys=True)) + print(dump_compact_json(compact)) return print(render_sync_report(payload), end="") @@ -416,7 +435,7 @@ def next_action_for_doctor(blocked_codes: list[str]) -> dict[str, object]: "action": "plan", "allowed": True, "requires_explicit_approval": False, - "command": "plan --targets --format report", + "command": "plan --targets ", "reason": "Doctor found no readiness blockers.", } @@ -449,7 +468,7 @@ def emit_output( compact_payload = build_compact_install_output(payload) if failure_message and "error" not in compact_payload: compact_payload["error"] = failure_message - print(json.dumps(compact_payload, indent=2, sort_keys=True)) + print(dump_compact_json(compact_payload)) return if format_name == "json": diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_output.py b/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_output.py index c8e9ff51..601b84f9 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_output.py +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_output.py @@ -1,5 +1,6 @@ from __future__ import annotations +import json from collections.abc import Iterable BLOCKER_REASON_MAP: dict[str, str] = { @@ -34,7 +35,11 @@ "bisync-residual-drift": "Post-apply bisync still detected residual drift.", } -REPORT_TABLE_ROW_LIMIT = 20 +REPORT_TABLE_ROW_LIMIT = 8 + + +def dump_compact_json(payload: dict[str, object]) -> str: + return json.dumps(payload, sort_keys=True, separators=(",", ":")) def _install_lane_label() -> str: @@ -46,28 +51,30 @@ def _bisync_lane_label() -> str: def build_compact_install_output(payload: dict[str, object]) -> dict[str, object]: + next_action = payload.get("next_action") compact: dict[str, object] = { "mode": payload.get("mode"), "status": payload.get("validation", payload.get("status")), - "blocked_codes": list(_as_iterable(payload.get("blocked_codes"))), - "next_action": payload.get("next_action"), - "next_step": payload.get("next_step"), - "selected_targets_count": _count_value(payload.get("selected_targets")), - "retired_targets_count": _count_value(payload.get("retired_targets")), - "source_resources_considered": payload.get("source_resources_considered"), - "copied_count": _count_value(payload.get("copied")), - "skipped_count": _count_value(payload.get("skipped")), - "blocked_count": _count_value(payload.get("blocked")), - "conflict_count": _count_value(payload.get("conflicts")), - "missing_dirs_count": _count_value(payload.get("missing_dirs")), - "residual_drift_count": _count_value(payload.get("residual_drift")), - "unsupported_families_count": _count_unsupported_families( - payload.get("unsupported_families_by_target") - ), - "changed_resources": _install_change_evidence(payload), + "targets": _as_string_list(payload.get("selected_targets")), + "next": _next_action_name(next_action), + "approval": _next_action_requires_approval(next_action), + "counts": { + "source": payload.get("source_resources_considered") or 0, + "copy": _count_value(payload.get("copied")), + "skip": _count_value(payload.get("skipped")), + "blocked": _count_value(payload.get("blocked")), + "conflict": _count_value(payload.get("conflicts")), + "missing_dir": _count_value(payload.get("missing_dirs")), + "residual": _count_value(payload.get("residual_drift")), + "unsupported": _count_unsupported_families(payload.get("unsupported_families_by_target")), + }, } - _copy_if_present(compact, payload, "state_path") - _copy_if_present(compact, payload, "manifest_path") + blockers = _as_string_list(payload.get("blocked_codes")) + if blockers: + compact["blockers"] = blockers + changes = _install_change_evidence(payload, limit=4) + if changes: + compact["changes"] = changes _copy_if_present(compact, payload, "error") return compact @@ -77,19 +84,24 @@ def build_compact_bisync_output(payload: dict[str, object]) -> dict[str, object] status = None if isinstance(verification, dict): status = verification.get("status") + next_action = payload.get("next_action") compact: dict[str, object] = { "mode": payload.get("mode"), "status": status if status is not None else payload.get("status"), - "blocked_codes": list(_as_iterable(payload.get("blocked_codes"))), - "next_action": payload.get("next_action"), - "next_step": payload.get("next_step"), - "drift_total": _count_value(payload.get("drifts")), - "direction_counts": _direction_counts(payload.get("drifts")), - "bucket_counts": _bucket_counts(payload.get("drifts")), - "changed_resources": _bisync_change_evidence(payload.get("drifts")), + "next": _next_action_name(next_action), + "approval": _next_action_requires_approval(next_action), + "counts": { + "drift": _count_value(payload.get("drifts")), + **_direction_counts(payload.get("drifts")), + **_bucket_counts(payload.get("drifts")), + }, } - _copy_if_present(compact, payload, "state_path") - _copy_if_present(compact, payload, "manifest_path") + blockers = _as_string_list(payload.get("blocked_codes")) + if blockers: + compact["blockers"] = blockers + changes = _bisync_change_evidence(payload.get("drifts"), limit=4) + if changes: + compact["changes"] = changes _copy_if_present(compact, payload, "error") if isinstance(verification, dict): _copy_if_present(compact, verification, "code") @@ -266,14 +278,16 @@ def _install_change_evidence(payload: dict[str, object], limit: int = 8) -> list if action not in interesting_actions: continue path = operation.get("path") + resource_id = operation.get("resource_id") if not isinstance(path, str) or not path: continue - key = (str(action), path) + resource = resource_id if isinstance(resource_id, str) and resource_id else _compact_path(path) + key = (str(action), str(resource)) if key in seen: continue seen.add(key) - item: dict[str, object] = {"action": action, "path": path} - for field in ("resource_id", "code", "reason"): + item: dict[str, object] = {"action": action, "resource": resource} + for field in ("code",): value = operation.get(field) if isinstance(value, str) and value: item[field] = value @@ -292,7 +306,7 @@ def _bisync_change_evidence(drifts: object, limit: int = 8) -> list[dict[str, ob if not isinstance(drift, dict): continue item: dict[str, object] = {} - for field in ("skill", "direction", "type", "winner", "reason"): + for field in ("skill", "direction", "type"): value = drift.get(field) if isinstance(value, str) and value: item[field] = value @@ -308,6 +322,7 @@ def _bisync_change_evidence(drifts: object, limit: int = 8) -> list[dict[str, ob def _sync_auto_applied_rows(install_payload: dict[str, object]) -> list[list[str]]: rows: list[list[str]] = [] + grouped: dict[tuple[str, str, str], int] = {} operations = install_payload.get("operations") if not isinstance(operations, list): return rows @@ -320,7 +335,10 @@ def _sync_auto_applied_rows(install_payload: dict[str, object]) -> list[list[str continue item = str(operation.get("resource_id") or operation.get("path") or "unknown") reason = str(operation.get("reason") or "Repo-to-home copy applied.") - rows.append([item, reason, verification]) + grouped[(item, reason, verification)] = grouped.get((item, reason, verification), 0) + 1 + for (item, reason, verification), count in grouped.items(): + label = f"{item} ({count} targets)" if count > 1 else item + rows.append([label, reason, verification]) return rows @@ -753,6 +771,7 @@ def _as_string_list(value: object) -> list[str]: def _install_planned_rows(payload: dict[str, object]) -> list[list[str]]: rows: list[list[str]] = [] + grouped: dict[tuple[str, str, str, str], int] = {} operations = payload.get("operations") if not isinstance(operations, list): return rows @@ -762,16 +781,20 @@ def _install_planned_rows(payload: dict[str, object]) -> list[list[str]]: action = str(operation.get("action") or "") if action not in {"copy", "mkdir", "delete", "stale-managed"}: continue - path = str(operation.get("path") or operation.get("resource_id") or "unknown") + path = str(operation.get("resource_id") or operation.get("path") or "unknown") reason = str(operation.get("reason") or "policy decision") winner = str(operation.get("code") or operation.get("target") or "n/a") planned = "copy" if action == "copy" else action - rows.append([path, "install", planned, reason, winner]) + grouped[(path, planned, reason, winner)] = grouped.get((path, planned, reason, winner), 0) + 1 + for (path, planned, reason, winner), count in grouped.items(): + label = f"{path} ({count} targets)" if count > 1 else path + rows.append([label, "install", planned, reason, winner]) return rows def _install_completed_rows(payload: dict[str, object]) -> list[list[str]]: rows: list[list[str]] = [] + grouped: dict[tuple[str, str, str, str, str], int] = {} if str(payload.get("mode") or "") != "apply": return rows operations = payload.get("operations") @@ -783,16 +806,20 @@ def _install_completed_rows(payload: dict[str, object]) -> list[list[str]]: action = str(operation.get("action") or "") if action not in {"copy", "delete", "mkdir"}: continue - path = str(operation.get("path") or operation.get("resource_id") or "unknown") + path = str(operation.get("resource_id") or operation.get("path") or "unknown") reason = str(operation.get("reason") or "applied by plan") result = "ok" if action != "skip" else "skipped" verification = "hash-match" if action in {"copy", "delete"} else "n/a" - rows.append([path, action, reason, result, verification]) + grouped[(path, action, reason, result, verification)] = grouped.get((path, action, reason, result, verification), 0) + 1 + for (path, action, reason, result, verification), count in grouped.items(): + label = f"{path} ({count} targets)" if count > 1 else path + rows.append([label, action, reason, result, verification]) return rows def _install_blocker_rows(payload: dict[str, object]) -> list[list[str]]: rows: list[list[str]] = [] + seen: set[tuple[str, str, str]] = set() operations = payload.get("operations") if isinstance(operations, list): for operation in operations: @@ -802,8 +829,12 @@ def _install_blocker_rows(payload: dict[str, object]) -> list[list[str]]: if action != "blocked": continue code = str(operation.get("code") or "unknown") - path = str(operation.get("path") or operation.get("resource_id") or "unknown") + path = str(operation.get("resource_id") or operation.get("path") or "unknown") reason = str(operation.get("reason") or _reason_for_code(code)) + key = (code, path, reason) + if key in seen: + continue + seen.add(key) rows.append([code, path, reason, "Resolve blocker and rerun plan."]) for code in _as_string_list(payload.get("blocked_codes")): if any(existing[0] == code for existing in rows): @@ -961,6 +992,19 @@ def _next_action_name(next_action: object) -> str: return "unknown" +def _next_action_requires_approval(next_action: object) -> bool: + if isinstance(next_action, dict): + return bool(next_action.get("requires_explicit_approval", False)) + return False + + +def _compact_path(path: str) -> str: + parts = [part for part in path.split("/") if part] + if len(parts) >= 2: + return "/".join(parts[-2:]) + return path + + def _reason_for_code(code: str) -> str: return BLOCKER_REASON_MAP.get(code, "Manual review required.") diff --git a/tests/test_home_ai_sync_output.py b/tests/test_home_ai_sync_output.py new file mode 100644 index 00000000..ec4dbd6a --- /dev/null +++ b/tests/test_home_ai_sync_output.py @@ -0,0 +1,57 @@ +import json +import sys +from pathlib import Path + +SCRIPT_DIR = Path( + ".github/skills/local-agent-sync-install-ai-resources/scripts" +).resolve() +sys.path.insert(0, SCRIPT_DIR.as_posix()) + +from sync_output import build_compact_install_output, dump_compact_json # noqa: E402 + + +def test_compact_install_output_is_single_line_and_bounded() -> None: + payload = { + "mode": "plan", + "validation": "blocked", + "selected_targets": ["skills", "codex"], + "source_resources_considered": 2, + "copied": ["/very/long/home/path/internal-one"], + "skipped": ["skip-one"], + "blocked": ["/very/long/home/path/internal-two"], + "blocked_codes": ["target-exists-unmanaged"], + "next_action": { + "action": "resolve_blockers", + "requires_explicit_approval": True, + }, + "operations": [ + { + "action": "copy", + "path": "/very/long/home/path/internal-one", + "resource_id": "internal-one", + "reason": "verbose reason omitted from compact output", + }, + { + "action": "blocked", + "path": "/very/long/home/path/internal-two", + "resource_id": "internal-two", + "code": "target-exists-unmanaged", + "reason": "verbose reason omitted from compact output", + }, + ], + } + + compact = build_compact_install_output(payload) + line = dump_compact_json(compact) + + assert "\n" not in line + assert json.loads(line)["next"] == "resolve_blockers" + assert compact["counts"]["blocked"] == 1 + assert compact["changes"] == [ + {"action": "copy", "resource": "internal-one"}, + { + "action": "blocked", + "code": "target-exists-unmanaged", + "resource": "internal-two", + }, + ] diff --git a/tests/test_local_agent_sync_install_ai_resources_contracts.py b/tests/test_local_agent_sync_install_ai_resources_contracts.py new file mode 100644 index 00000000..d293094f --- /dev/null +++ b/tests/test_local_agent_sync_install_ai_resources_contracts.py @@ -0,0 +1,233 @@ +import argparse +import subprocess +import sys +import textwrap +import tomllib +from pathlib import Path +from types import SimpleNamespace + +import pytest + +SCRIPT_DIR = Path( + ".github/skills/local-agent-sync-install-ai-resources/scripts" +).resolve() +sys.path.insert(0, SCRIPT_DIR.as_posix()) + +from agent_translation import target_extension, translate_agent_for_target # noqa: E402 +from bisync_skills import build_bisync_plan # noqa: E402 +from home_sync_contract import load_home_sync_catalog, load_home_sync_policy # noqa: E402 +from home_syncing import HomeSyncOperation, parse_targets # noqa: E402 +from sync_home_ai_resources import ( # noqa: E402 + bisync_requires_review, + install_auto_apply_blockers, +) + + +def test_translate_agent_for_codex_preserves_body_and_handoffs(tmp_path: Path) -> None: + source_path = tmp_path / "review.agent.md" + source_path.write_text( + textwrap.dedent( + """\ + --- + name: review-agent + description: Review changes carefully. + handoffs: + - label: Escalate + agent: internal-code-review + prompt: Include the risky files. + --- + Main body instructions. + """ + ), + encoding="utf-8", + ) + + translated = translate_agent_for_target(source_path, "codex") + payload = tomllib.loads(translated) + + assert target_extension("codex") == ".toml" + assert payload["name"] == "review-agent" + assert "Main body instructions." in payload["developer_instructions"] + assert "## Handoffs" in payload["developer_instructions"] + assert "internal-code-review" in payload["developer_instructions"] + + +def test_load_home_sync_catalog_autodiscovers_skills_and_honors_policy(tmp_path: Path) -> None: + refs_dir = ( + tmp_path + / ".github" + / "skills" + / "local-agent-sync-install-ai-resources" + / "references" + ) + refs_dir.mkdir(parents=True) + (refs_dir / "home-sync-catalog.yaml").write_text( + textwrap.dedent( + """\ + version: 1 + defaults: + include_internal_skills: true + include_local_skills: false + include_unlisted_skills: true + unmanaged_existing_skills_policy: repo-wins + excluded_skills: + - graphify + skill_targets: + - codex + - copilot + resources: [] + """ + ), + encoding="utf-8", + ) + + skills_root = tmp_path / ".github" / "skills" + for skill_name in ("alpha-skill", "internal-gamma", "local-beta", "graphify"): + skill_dir = skills_root / skill_name + skill_dir.mkdir(parents=True, exist_ok=True) + (skill_dir / "SKILL.md").write_text(f"# {skill_name}\n", encoding="utf-8") + + policy = load_home_sync_policy(tmp_path) + catalog = load_home_sync_catalog(tmp_path) + + assert policy.unmanaged_existing_skills_policy == "repo-wins" + assert policy.excluded_skills == ("graphify",) + assert {resource.resource_id for resource in catalog} == { + "alpha-skill", + "internal-gamma", + } + assert all(resource.include_targets == ("codex", "copilot") for resource in catalog) + + +def test_parse_targets_orders_cross_aliases_and_rejects_unknown() -> None: + assert parse_targets("copilot,skills") == ("skills", "copilot") + assert parse_targets("tutto") == ("skills", "codex", "copilot", "opencode") + + with pytest.raises(ValueError, match="unknown-target: invalid"): + parse_targets("skills,invalid") + + +def test_build_bisync_plan_filters_local_and_excluded_bundles(tmp_path: Path) -> None: + refs_dir = ( + tmp_path + / ".github" + / "skills" + / "local-agent-sync-install-ai-resources" + / "references" + ) + refs_dir.mkdir(parents=True) + (refs_dir / "home-sync-catalog.yaml").write_text( + textwrap.dedent( + """\ + version: 1 + defaults: + include_internal_skills: true + include_local_skills: false + include_unlisted_skills: false + unmanaged_existing_skills_policy: block + excluded_skills: + - graphify + skill_targets: + - codex + resources: [] + """ + ), + encoding="utf-8", + ) + + repo_skills = tmp_path / ".github" / "skills" + home_root = tmp_path / "home" + home_skills = home_root / ".agents" / "skills" + home_skills.mkdir(parents=True) + + for skill_name, root in ( + ("shared-skill", repo_skills), + ("local-helper", repo_skills), + ("graphify", repo_skills), + ("home-only-skill", home_skills), + ): + skill_dir = root / skill_name + skill_dir.mkdir(parents=True, exist_ok=True) + (skill_dir / "SKILL.md").write_text(f"# {skill_name}\n", encoding="utf-8") + + plan = build_bisync_plan(tmp_path, home_root, mode="plan") + + assert {(drift.skill_name, drift.drift_type) for drift in plan.drifts} == { + ("shared-skill", "only-repo"), + ("home-only-skill", "only-home"), + } + assert "bisync-only-home" in plan.blocked_codes + assert "bisync-only-repo" in plan.blocked_codes + assert all(drift.skill_name not in {"local-helper", "graphify"} for drift in plan.drifts) + + +def test_install_auto_apply_blockers_require_explicit_review() -> None: + class DummyPlan: + def __init__(self) -> None: + self.operations = ( + HomeSyncOperation( + target="skills", + action="mkdir", + path="/tmp/home/.agents/skills", + reason="Missing runtime root.", + ), + HomeSyncOperation( + target="skills", + action="warning", + path="/tmp/home/.agents/skills/demo", + reason="Home drift persists.", + code="install-warning", + ), + ) + + def blocked_codes(self) -> list[str]: + return [] + + blockers = install_auto_apply_blockers( + DummyPlan(), + argparse.Namespace(create_missing_dirs=False), + ) + + assert blockers == ["install-residual-drift", "needs-directory-create"] + + +def test_bisync_requires_review_only_for_non_safe_drift() -> None: + safe_plan = SimpleNamespace( + blocked_codes=["bisync-only-repo"], + drifts=[SimpleNamespace(drift_type="only-repo", direction="repo-to-home")], + ) + review_plan = SimpleNamespace( + blocked_codes=[], + drifts=[SimpleNamespace(drift_type="drift", direction="home-to-repo")], + ) + + assert bisync_requires_review(safe_plan) is False + assert bisync_requires_review(review_plan) is True + + +def test_skill_run_sh_should_quiet_only_for_compact_modes() -> None: + run_sh = ( + Path(".github/skills/local-agent-sync-install-ai-resources/scripts/run.sh") + .resolve() + .as_posix() + ) + script = textwrap.dedent( + f"""\ + source <(sed '$d' "{run_sh}") + should_quiet --format compact + compact_status="$?" + should_quiet --format text + text_status="$?" + printf '%s,%s\\n' "$compact_status" "$text_status" + """ + ) + + result = subprocess.run( + ["bash", "-lc", script], + text=True, + capture_output=True, + check=False, + ) + + assert result.returncode == 0 + assert result.stdout.strip() == "0,1" diff --git a/tests/test_run_sh_dispatch.py b/tests/test_run_sh_dispatch.py index 6ff96cb8..eeaadd45 100644 --- a/tests/test_run_sh_dispatch.py +++ b/tests/test_run_sh_dispatch.py @@ -18,7 +18,8 @@ def test_resolve_script_handles_validator_and_debug_log_tools() -> None: result = run_shell( "source ./.github/scripts/run.sh; " "printf '%s\\n' \"$(resolve_script validate_critical_output)\"; " - "printf '%s\\n' \"$(resolve_script analyze_copilot_debug_log)\"" + "printf '%s\\n' \"$(resolve_script analyze_copilot_debug_log)\"; " + "printf '%s\\n' \"$(resolve_script sync_home_ai_resources)\"" ) assert result.returncode == 0 lines = [line.strip() for line in result.stdout.splitlines() if line.strip()] @@ -26,3 +27,6 @@ def test_resolve_script_handles_validator_and_debug_log_tools() -> None: ".github/skills/internal-gateway-critical-master/scripts/validate_critical_output.py" ) assert lines[1].endswith("tools/analyze_copilot_debug_log/run.sh") + assert lines[2].endswith( + ".github/skills/local-agent-sync-install-ai-resources/scripts/run.sh" + ) From 0641dfaae0b34a15176dc5d8e00505aa1ea1519a Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Mon, 6 Jul 2026 18:31:01 +0200 Subject: [PATCH 02/59] fix: ensure main function is called correctly in run.sh and update test script for compatibility --- .../local-agent-sync-install-ai-resources/scripts/run.sh | 4 +++- tests/test_local_agent_sync_install_ai_resources_contracts.py | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh b/.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh index 78f0dc26..61fb0d2a 100755 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh @@ -119,4 +119,6 @@ PYTHON_BIN="${PYTHON_BIN:-python3}" REQUIREMENTS_FILE="$SCRIPT_DIR/requirements.txt" REQUIREMENTS_HASH_FILE="$VENV_DIR/.requirements.sha256" -main "$@" +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main "$@" +fi diff --git a/tests/test_local_agent_sync_install_ai_resources_contracts.py b/tests/test_local_agent_sync_install_ai_resources_contracts.py index d293094f..608f19a6 100644 --- a/tests/test_local_agent_sync_install_ai_resources_contracts.py +++ b/tests/test_local_agent_sync_install_ai_resources_contracts.py @@ -213,7 +213,8 @@ def test_skill_run_sh_should_quiet_only_for_compact_modes() -> None: ) script = textwrap.dedent( f"""\ - source <(sed '$d' "{run_sh}") + source "{run_sh}" + set +e should_quiet --format compact compact_status="$?" should_quiet --format text From a9cffdc9d438edc98061be5753b9ba7b051c4e41 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Mon, 6 Jul 2026 21:56:57 +0200 Subject: [PATCH 03/59] feat: implement bisync plan snapshot functionality and enforce review requirement for bisync apply --- .../SKILL.md | 5 +- .../references/error-codes.md | 1 + .../references/sync-contract.md | 12 +- .../scripts/bisync_skills.py | 61 +++- .../scripts/home_syncing.py | 54 +++- .../scripts/sync_output.py | 1 + ...ent_sync_install_ai_resources_contracts.py | 293 +++++++++++++++++- 7 files changed, 415 insertions(+), 12 deletions(-) diff --git a/.github/skills/local-agent-sync-install-ai-resources/SKILL.md b/.github/skills/local-agent-sync-install-ai-resources/SKILL.md index a15a2ba2..ea69fd24 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/SKILL.md +++ b/.github/skills/local-agent-sync-install-ai-resources/SKILL.md @@ -57,9 +57,10 @@ may be used for compatibility; it delegates to the bundled skill runner. - `doctor`: read-only readiness checks for runtime roots, support matrix, catalog paths, and sync state. - `plan` or `dry-run`: install-lane dry run. - `audit`: compare source, manifest, and managed target paths without writing runtime files. +- `--fast`: read-only shortcut for `plan` and `audit` only. Write modes still evaluate the full source catalog. - `apply`: explicit install-lane materialization. Never run from `next_action` alone. - `bisync plan`: read-only drift detection between `.github/skills/` and `~/.agents/skills/`. -- `bisync apply`: explicit bidirectional drift resolution after reviewed plan and clean repo preflight. +- `bisync apply`: explicit bidirectional drift resolution after a reviewed matching `bisync plan` snapshot and clean repo preflight. ### Default Sync Sequence @@ -82,7 +83,7 @@ Stop and report when any of these occur: - `next_action.requires_explicit_approval` is `true` and the user has not explicitly approved, except for the `sync` command's built-in install-lane auto-execute path. - `sync` reports install-lane residual drift, missing directory creation without `--create-missing-dirs`, stale managed resources, or any install blocker. - `sync` reports `home-to-repo`, `only-home`, `equal-mtime`, or another non-safe bisync blocker after install. Treat this as a review state, not an apply failure. -- `bisync apply` was requested without a prior `bisync plan`. +- `bisync apply` was requested without a prior matching reviewed `bisync plan` snapshot. - The source repository has uncommitted or untracked changes during `bisync apply`. - After `bisync apply` modifies `~/.agents/skills/` files that the install lane also manages, re-run install `plan`. Verified repo-to-home bisync copies refresh the manifest state; if `target-modified-managed` still appears, treat it as a real local divergence and review the path instead of deleting it as a routine recovery step. - If `bisync apply` is blocked by `bisync-repo-dirty` and the local workspace has unrelated uncommitted changes, run bisync from a clean detached worktree at the same commit and pass it through `--source-root`. diff --git a/.github/skills/local-agent-sync-install-ai-resources/references/error-codes.md b/.github/skills/local-agent-sync-install-ai-resources/references/error-codes.md index 2194a1d1..2f38d040 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/references/error-codes.md +++ b/.github/skills/local-agent-sync-install-ai-resources/references/error-codes.md @@ -31,6 +31,7 @@ When rendering a user-visible report, convert each active code into a plain-lang | `bisync-repo-dirty` | The source repository has uncommitted or untracked changes detected by `git status --porcelain --untracked-files=all`. | Block bisync apply. | Writing to a dirty repository risks overwriting uncommitted work and makes post-apply verification unreliable. | | `bisync-repo-git-failed` | The `git status` command failed on the source repository. | Block bisync apply. | Without a reliable dirty-repo check, bisync cannot guarantee safe writes to the repository side. | | `bisync-only-repo` | A skill bundle exists in the source repo but not in the home directory. | Non-blocking bisync status; explicit apply may create the home bundle. | Repo-to-home creation is allowed for valid non-excluded bundles, but still requires explicit apply approval. | +| `bisync-plan-required` | No reviewed matching bisync plan snapshot exists for the current repo/home drift set. | Block bisync apply and rerun `bisync plan`. | Bisync apply must write only from a drift set that was just reviewed; otherwise the write could target stale or changed drift decisions. | | `bisync-only-home` | A skill bundle exists in the home directory but not in the source repo. | Block bisync apply; require manual resolution. | Automatic resolution would mean deleting the home copy or creating a new repo copy without user intent. | | `bisync-equal-mtime` | Hashes differ between repo and home, but mtime is equal for both sides. | Block bisync apply; require manual resolution. | When mtime is equal, the tool cannot determine which side is newer; the user must decide the direction. | | `bisync-verify-failed` | Post-copy hash verification failed for a specific skill. | Block bisync apply; report the failing skill. | A hash mismatch after copy indicates a copy error, filesystem issue, or concurrent modification during apply. | diff --git a/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md b/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md index c2497078..2e1baea3 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md +++ b/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md @@ -19,6 +19,7 @@ Expected state files: - `manifest.json` - `last-plan.json` - `last-audit.json` +- `last-bisync-plan.json` - `locks/home-ai-resources.lock` Optional debug logs may live under `logs/` when the implementation needs durable operator evidence. @@ -223,6 +224,7 @@ The install lane provides unidirectional `repo -> home` materialization of allow - `sync`: safe top-level automation that may apply only clean repo-to-home install work before running the bisync review gate. - `plan`: dry run that produces a readable diff and machine-readable state. Read-only. - `audit`: compare source, manifest, and managed target paths. Read-only. +- `--fast`: manifest-focused shortcut for `plan` and `audit` only. It must not change `apply` or `sync` source discovery. - `doctor`: verify runtime roots, permissions, symlink posture, and manifest health. Read-only. - `apply`: materialize approved operations. Writes to home only. - `dry-run`: alias of `plan`. @@ -252,7 +254,7 @@ The `bisync` lane provides explicit bidirectional reconciliation between `.githu ### Bisync Modes - `bisync plan`: detect drift. Read-only. Produces a drift list with entries for `repo-to-home`, `home-to-repo`, `only-repo`, `only-home`, and `equal-mtime`. -- `bisync apply`: resolve drift by copying winner to loser. Writes to both repo and home as needed. +- `bisync apply`: resolve drift by copying winner to loser. Writes to both repo and home as needed, but only after a reviewed matching `bisync plan` snapshot exists for the same repo and home roots. ### Logic @@ -274,9 +276,10 @@ The `bisync` lane provides explicit bidirectional reconciliation between `.githu Before any write in `bisync apply`: -1. Verify `git status --porcelain --untracked-files=all` on the source repository. -2. Block `apply` if the repository is not clean. -3. Block `apply` if any `only-home` or `equal-mtime` entry exists in the current plan. +1. Require a reviewed `last-bisync-plan.json` snapshot that matches the current drift plan for the same repo and home roots. +2. Verify `git status --porcelain --untracked-files=all` on the source repository. +3. Block `apply` if the repository is not clean. +4. Block `apply` if any `only-home` or `equal-mtime` entry exists in the current plan. ### Apply @@ -305,3 +308,4 @@ The `bisync` payload includes: - `next_step`: human-readable next instruction. - `next_action`: structured object with `action`, `allowed`, `requires_explicit_approval`, `command`, `reason`. - `verification`: post-apply status with `status` and optional `reason` or `residual_drifts`. +- `bisync-plan-required`: emitted when `bisync apply` is requested without a matching reviewed snapshot. diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/bisync_skills.py b/.github/skills/local-agent-sync-install-ai-resources/scripts/bisync_skills.py index 2f2cf6cc..e5e2bd3b 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/bisync_skills.py +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/bisync_skills.py @@ -19,7 +19,7 @@ from dataclasses import dataclass, field from pathlib import Path -from home_sync_contract import load_home_sync_policy +from home_sync_contract import load_home_sync_policy, state_root_for_home from home_syncing import reconcile_manifest_entry_after_bisync_copy from sync_output import ( build_compact_bisync_output, @@ -30,6 +30,7 @@ IGNORED_SYNC_PARTS: tuple[str, ...] = (".venv", "__pycache__", ".pytest_cache") IGNORED_SYNC_SUFFIXES: tuple[str, ...] = (".pyc", ".pyo") EXCLUDED_BUNDLE_PREFIX: str = "local-" +BISYNC_PLAN_PATH = "last-bisync-plan.json" def should_ignore(path: Path) -> bool: @@ -505,6 +506,44 @@ def apply_bisync_plan( return plan +def write_bisync_plan_snapshot(plan: BisyncPlan) -> Path: + state_root = state_root_for_home(plan.home_root) + state_root.mkdir(parents=True, exist_ok=True) + snapshot_path = state_root / BISYNC_PLAN_PATH + snapshot = { + "source_root": plan.source_root.as_posix(), + "home_root": plan.home_root.as_posix(), + "drifts": [drift.to_dict() for drift in plan.drifts], + "blocked_codes": plan.blocked_codes, + } + snapshot_path.write_text(json.dumps(snapshot, indent=2, sort_keys=True), encoding="utf-8") + return snapshot_path + + +def load_bisync_plan_snapshot(source_root: Path, home_root: Path) -> dict[str, object] | None: + snapshot_path = state_root_for_home(home_root) / BISYNC_PLAN_PATH + if not snapshot_path.exists(): + return None + try: + snapshot = json.loads(snapshot_path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return None + if not isinstance(snapshot, dict): + return None + if snapshot.get("source_root") != source_root.as_posix(): + return None + if snapshot.get("home_root") != home_root.as_posix(): + return None + return snapshot + + +def reviewed_bisync_plan_matches(plan: BisyncPlan, snapshot: dict[str, object]) -> bool: + return ( + snapshot.get("drifts") == [drift.to_dict() for drift in plan.drifts] + and snapshot.get("blocked_codes") == plan.blocked_codes + ) + + def _next_step_for_bisync(plan: BisyncPlan) -> str: if plan.mode == "plan": if plan.blocked_codes: @@ -538,6 +577,7 @@ def run_bisync_plan(args: argparse.Namespace) -> int: source_root = _resolve_source_root(args) home_root = Path(args.home_root).expanduser().resolve() plan = build_bisync_plan(source_root, home_root, mode="plan") + write_bisync_plan_snapshot(plan) _emit_bisync_output(plan, args.format) return 1 if plan.blocked_codes else 0 @@ -546,6 +586,25 @@ def run_bisync_apply(args: argparse.Namespace) -> int: source_root = _resolve_source_root(args) home_root = Path(args.home_root).expanduser().resolve() plan = build_bisync_plan(source_root, home_root, mode="plan") + snapshot = load_bisync_plan_snapshot(source_root, home_root) + if snapshot is None or not reviewed_bisync_plan_matches(plan, snapshot): + plan.mode = "apply" + plan.blocked_codes = sorted(set([*plan.blocked_codes, "bisync-plan-required"])) + plan.next_step = "Run bisync plan first, review that exact drift snapshot, then rerun bisync apply." + plan.next_action = { + "action": "review", + "allowed": False, + "requires_explicit_approval": True, + "command": f"bisync plan --source-root {source_root} --home-root {home_root}", + "reason": "Bisync apply requires a reviewed matching plan snapshot before any write.", + } + plan.verification = { + "status": "blocked", + "code": "bisync-plan-required", + "reason": "Bisync apply requires a reviewed matching plan snapshot.", + } + _emit_bisync_output(plan, args.format) + return 1 resolvable_during_apply = {"bisync-only-repo"} remaining_blockers = [ code for code in plan.blocked_codes if code not in resolvable_during_apply diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/home_syncing.py b/.github/skills/local-agent-sync-install-ai-resources/scripts/home_syncing.py index 85ba7686..a2d650e6 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/home_syncing.py +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/home_syncing.py @@ -194,8 +194,15 @@ def build_home_sync_plan( unsupported_families_by_target: dict[str, set[str]] = {target: set() for target in targets} add_manifest_state_operation(operations, state_root, mode, manifest_error) - catalog_resources = filter_catalog_for_fast_mode(catalog, manifest_payload, targets, fast=fast) + catalog_resources = filter_catalog_for_fast_mode( + catalog, + manifest_payload, + targets, + mode=mode, + fast=fast, + ) add_target_root_operations(operations, missing_dirs, home_root, targets, mode) + seen_target_paths: set[str] = set() for resource in catalog_resources: source_path = source_root / resource.source_path @@ -256,6 +263,9 @@ def build_home_sync_plan( content_hash=content_hash, last_action="copy", ) + if managed_resource.target_path in seen_target_paths: + continue + seen_target_paths.add(managed_resource.target_path) desired_resources.append(managed_resource) add_materialization_operation( operations, @@ -280,6 +290,8 @@ def build_home_sync_plan( home_root, policy, ) + operations = list(dedupe_operations_by_identity(operations)) + desired_resources = list(dedupe_resources_by_target_path(desired_resources)) return HomeSyncPlan( source_root=source_root, home_root=home_root, @@ -481,8 +493,11 @@ def filter_catalog_for_fast_mode( manifest_payload: dict[str, object], targets: tuple[str, ...], *, + mode: str, fast: bool, ) -> list[CatalogResource]: + if mode not in {"plan", "audit"}: + return catalog if not fast or not manifest_payload.get("managed_resources"): return catalog resource_ids = { @@ -493,6 +508,30 @@ def filter_catalog_for_fast_mode( return [resource for resource in catalog if resource.resource_id in resource_ids] +def dedupe_operations_by_identity( + operations: list[HomeSyncOperation], +) -> tuple[HomeSyncOperation, ...]: + ordered: dict[tuple[str, str, str | None, str | None], HomeSyncOperation] = {} + for operation in operations: + key = ( + operation.action, + operation.path, + operation.code, + operation.resource_id, + ) + ordered.setdefault(key, operation) + return tuple(ordered.values()) + + +def dedupe_resources_by_target_path( + resources: list[ManagedResource], +) -> tuple[ManagedResource, ...]: + ordered: dict[str, ManagedResource] = {} + for resource in resources: + ordered.setdefault(resource.target_path, resource) + return tuple(ordered.values()) + + def add_target_root_operations( operations: list[HomeSyncOperation], missing_dirs: list[str], @@ -500,12 +539,17 @@ def add_target_root_operations( targets: tuple[str, ...], mode: str, ) -> None: + seen_roots: set[str] = set() for target in targets: skill_root = runtime_skill_root(home_root, target) - _add_root_operation(operations, missing_dirs, home_root, target, skill_root, mode) + if skill_root.as_posix() not in seen_roots: + seen_roots.add(skill_root.as_posix()) + _add_root_operation(operations, missing_dirs, home_root, target, skill_root, mode) if has_agent_root(target): agent_root = runtime_agent_root(home_root, target) - _add_root_operation(operations, missing_dirs, home_root, target, agent_root, mode) + if agent_root.as_posix() not in seen_roots: + seen_roots.add(agent_root.as_posix()) + _add_root_operation(operations, missing_dirs, home_root, target, agent_root, mode) def _add_root_operation( @@ -1168,9 +1212,13 @@ def build_manifest_payload( ) -> dict[str, object]: blocked_paths = {operation.path for operation in plan.operations if operation.action == "blocked"} managed_resources = [] + seen_target_paths: set[str] = set() for resource in plan.desired_resources: if resource.target_path in blocked_paths: continue + if resource.target_path in seen_target_paths: + continue + seen_target_paths.add(resource.target_path) entry = resource.to_dict() if actual_content_hashes and resource.target_path in actual_content_hashes: entry["content_hash"] = actual_content_hashes[resource.target_path] diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_output.py b/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_output.py index 601b84f9..5c310831 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_output.py +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_output.py @@ -33,6 +33,7 @@ "bisync-verify-failed": "Post-copy hash verification failed.", "bisync-manifest-reconcile-failed": "Manifest reconciliation failed after a verified bisync copy.", "bisync-residual-drift": "Post-apply bisync still detected residual drift.", + "bisync-plan-required": "Bisync apply requires a reviewed plan snapshot that matches the current drift set.", } REPORT_TABLE_ROW_LIMIT = 8 diff --git a/tests/test_local_agent_sync_install_ai_resources_contracts.py b/tests/test_local_agent_sync_install_ai_resources_contracts.py index 608f19a6..69953aba 100644 --- a/tests/test_local_agent_sync_install_ai_resources_contracts.py +++ b/tests/test_local_agent_sync_install_ai_resources_contracts.py @@ -1,4 +1,5 @@ import argparse +import json import subprocess import sys import textwrap @@ -14,9 +15,15 @@ sys.path.insert(0, SCRIPT_DIR.as_posix()) from agent_translation import target_extension, translate_agent_for_target # noqa: E402 -from bisync_skills import build_bisync_plan # noqa: E402 +from bisync_skills import build_bisync_plan, run_bisync_apply, run_bisync_plan # noqa: E402 from home_sync_contract import load_home_sync_catalog, load_home_sync_policy # noqa: E402 -from home_syncing import HomeSyncOperation, parse_targets # noqa: E402 +from home_syncing import ( # noqa: E402 + HomeSyncOperation, + build_manifest_payload, + build_home_sync_plan, + parse_targets, + state_root_for_home, +) from sync_home_ai_resources import ( # noqa: E402 bisync_requires_review, install_auto_apply_blockers, @@ -232,3 +239,285 @@ def test_skill_run_sh_should_quiet_only_for_compact_modes() -> None: assert result.returncode == 0 assert result.stdout.strip() == "0,1" + + +def test_fast_mode_does_not_filter_apply_catalog(tmp_path: Path) -> None: + refs_dir = ( + tmp_path + / ".github" + / "skills" + / "local-agent-sync-install-ai-resources" + / "references" + ) + refs_dir.mkdir(parents=True) + (refs_dir / "home-sync-catalog.yaml").write_text( + textwrap.dedent( + """\ + version: 1 + defaults: + include_internal_skills: true + include_local_skills: false + include_unlisted_skills: true + unmanaged_existing_skills_policy: repo-wins + excluded_skills: [] + skill_targets: + - codex + resources: [] + """ + ), + encoding="utf-8", + ) + (refs_dir / "runtime-support-matrix.yaml").write_text( + textwrap.dedent( + """\ + version: 1 + rows: + - target: skills + resource_family: skills + support_level: Documented + home_path: ~/.agents/skills// + direct_copy_possible: true + translation_required: false + include_in_v1: true + evidence: [] + notes: Shared support + """ + ), + encoding="utf-8", + ) + + for skill_name in ("alpha-skill", "beta-skill"): + skill_dir = tmp_path / ".github" / "skills" / skill_name + skill_dir.mkdir(parents=True, exist_ok=True) + (skill_dir / "SKILL.md").write_text(f"# {skill_name}\n", encoding="utf-8") + + home_root = tmp_path / "home" + state_root = state_root_for_home(home_root) + state_root.mkdir(parents=True) + (state_root / "manifest.json").write_text( + json.dumps( + { + "managed_resources": [ + { + "target": "skills", + "resource_family": "skills", + "resource_id": "alpha-skill", + "target_path": str(home_root / ".agents/skills/alpha-skill"), + } + ] + } + ), + encoding="utf-8", + ) + + normal = build_home_sync_plan(tmp_path, home_root, ("skills",), mode="apply", fast=False) + fast = build_home_sync_plan(tmp_path, home_root, ("skills",), mode="apply", fast=True) + + assert fast.source_resources_considered == normal.source_resources_considered == 2 + + +def test_bisync_apply_requires_reviewed_plan_snapshot( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + repo_root = tmp_path / "repo" + home_root = tmp_path / "home" + refs_dir = ( + repo_root + / ".github" + / "skills" + / "local-agent-sync-install-ai-resources" + / "references" + ) + refs_dir.mkdir(parents=True) + (refs_dir / "home-sync-catalog.yaml").write_text( + textwrap.dedent( + """\ + version: 1 + defaults: + include_internal_skills: true + include_local_skills: false + include_unlisted_skills: false + unmanaged_existing_skills_policy: block + excluded_skills: [] + skill_targets: + - codex + resources: [] + """ + ), + encoding="utf-8", + ) + (repo_root / ".github" / "skills" / "demo-skill").mkdir(parents=True) + (repo_root / ".github" / "skills" / "demo-skill" / "SKILL.md").write_text( + "# demo\n", encoding="utf-8" + ) + (home_root / ".agents" / "skills").mkdir(parents=True) + + subprocess.run(["git", "init", "-q"], cwd=repo_root, check=True) + subprocess.run(["git", "config", "user.email", "review@example.com"], cwd=repo_root, check=True) + subprocess.run(["git", "config", "user.name", "reviewer"], cwd=repo_root, check=True) + subprocess.run(["git", "add", "."], cwd=repo_root, check=True) + subprocess.run(["git", "commit", "-qm", "init"], cwd=repo_root, check=True) + + args = argparse.Namespace( + source_root=repo_root.as_posix(), + home_root=home_root.as_posix(), + format="compact", + compact=True, + ) + + assert run_bisync_apply(args) == 1 + payload = json.loads(capsys.readouterr().out) + assert "bisync-plan-required" in payload["blockers"] + + +def test_bisync_apply_uses_matching_reviewed_plan_snapshot( + tmp_path: Path, capsys: pytest.CaptureFixture[str] +) -> None: + repo_root = tmp_path / "repo" + home_root = tmp_path / "home" + refs_dir = ( + repo_root + / ".github" + / "skills" + / "local-agent-sync-install-ai-resources" + / "references" + ) + refs_dir.mkdir(parents=True) + (refs_dir / "home-sync-catalog.yaml").write_text( + textwrap.dedent( + """\ + version: 1 + defaults: + include_internal_skills: true + include_local_skills: false + include_unlisted_skills: false + unmanaged_existing_skills_policy: block + excluded_skills: [] + skill_targets: + - codex + resources: [] + """ + ), + encoding="utf-8", + ) + (repo_root / ".github" / "skills" / "demo-skill").mkdir(parents=True) + (repo_root / ".github" / "skills" / "demo-skill" / "SKILL.md").write_text( + "# demo\n", encoding="utf-8" + ) + (home_root / ".agents" / "skills").mkdir(parents=True) + + subprocess.run(["git", "init", "-q"], cwd=repo_root, check=True) + subprocess.run(["git", "config", "user.email", "review@example.com"], cwd=repo_root, check=True) + subprocess.run(["git", "config", "user.name", "reviewer"], cwd=repo_root, check=True) + subprocess.run(["git", "add", "."], cwd=repo_root, check=True) + subprocess.run(["git", "commit", "-qm", "init"], cwd=repo_root, check=True) + + args = argparse.Namespace( + source_root=repo_root.as_posix(), + home_root=home_root.as_posix(), + format="compact", + compact=True, + ) + + assert run_bisync_plan(args) == 1 + _ = capsys.readouterr() + + assert run_bisync_apply(args) == 0 + assert (home_root / ".agents" / "skills" / "demo-skill" / "SKILL.md").is_file() + + +def test_cross_target_skill_plan_deduplicates_shared_paths( + tmp_path: Path, +) -> None: + refs_dir = ( + tmp_path + / ".github" + / "skills" + / "local-agent-sync-install-ai-resources" + / "references" + ) + refs_dir.mkdir(parents=True) + (refs_dir / "home-sync-catalog.yaml").write_text( + textwrap.dedent( + """\ + version: 1 + defaults: + include_internal_skills: true + include_local_skills: false + include_unlisted_skills: true + unmanaged_existing_skills_policy: repo-wins + excluded_skills: [] + skill_targets: + - codex + - copilot + - opencode + resources: [] + """ + ), + encoding="utf-8", + ) + (refs_dir / "runtime-support-matrix.yaml").write_text( + textwrap.dedent( + """\ + version: 1 + rows: + - target: skills + resource_family: skills + support_level: Documented + home_path: ~/.agents/skills// + direct_copy_possible: true + translation_required: false + include_in_v1: true + evidence: [] + notes: Shared support + - target: codex + resource_family: skills + support_level: Documented + home_path: ~/.agents/skills// + direct_copy_possible: true + translation_required: false + include_in_v1: true + evidence: [] + notes: Shared support + - target: copilot + resource_family: skills + support_level: Documented + home_path: ~/.agents/skills// + direct_copy_possible: true + translation_required: false + include_in_v1: true + evidence: [] + notes: Shared support + - target: opencode + resource_family: skills + support_level: Documented + home_path: ~/.agents/skills// + direct_copy_possible: true + translation_required: false + include_in_v1: true + evidence: [] + notes: Shared support + """ + ), + encoding="utf-8", + ) + skill_dir = tmp_path / ".github" / "skills" / "demo-skill" + skill_dir.mkdir(parents=True, exist_ok=True) + (skill_dir / "SKILL.md").write_text("# demo-skill\n", encoding="utf-8") + + plan = build_home_sync_plan( + tmp_path, + tmp_path / "home", + ("skills", "codex", "copilot", "opencode"), + mode="plan", + ) + + copy_paths = [operation.path for operation in plan.operations if operation.action == "copy"] + assert copy_paths.count(str((tmp_path / "home" / ".agents" / "skills" / "demo-skill").resolve())) == 1 + + manifest_payload = build_manifest_payload(plan) + desired_paths = [ + resource["target_path"] for resource in manifest_payload["managed_resources"] + ] + assert desired_paths.count(str((tmp_path / "home" / ".agents" / "skills" / "demo-skill").resolve())) == 1 + assert plan.source_resources_considered == 1 From 3ceb6f0753d34362bc3f0ea79c89a8fd85b01bc1 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Mon, 6 Jul 2026 22:37:18 +0200 Subject: [PATCH 04/59] Refactor support routing and method suggestion scripts - Updated support routing documentation to clarify guidance and streamline core rules. - Simplified evidence order and method buckets for clearer decision-making. - Enhanced claim discipline and added advisory helpers for better task resolution. - Refined the resolve_simple_task.py script to improve clarity and reduce complexity in gate and claim handling. - Updated suggest_support_skills.py to provide more generic method hints and improved path suggestion logic. - Removed deprecated skill references and streamlined the suggestion process for better usability. --- .../internal-gateway-simple-task.agent.md | 15 +- .../internal-gateway-simple-task/SKILL.md | 370 ++++++----------- .../agents/openai.yaml | 2 +- .../references/clarification-gate.md | 96 ++--- .../references/plan-mode.md | 152 ++----- .../references/simple-lanes.md | 91 ++--- .../references/support-routing.md | 119 ++---- .../scripts/resolve_simple_task.py | 383 +++++++----------- .../scripts/suggest_support_skills.py | 209 ++-------- 9 files changed, 456 insertions(+), 981 deletions(-) diff --git a/.github/agents/internal-gateway-simple-task.agent.md b/.github/agents/internal-gateway-simple-task.agent.md index c78eb7ad..44db7a10 100644 --- a/.github/agents/internal-gateway-simple-task.agent.md +++ b/.github/agents/internal-gateway-simple-task.agent.md @@ -1,22 +1,9 @@ --- name: internal-gateway-simple-task -description: "Use this agent when a concrete low-to-medium-risk repository-owned task can be answered, edited, diagnosed, validated, or planned quickly, or switched to plan mode to produce a retained plan instead of same-chat execution." +description: "Use this agent when a concrete low-to-medium-risk repository-owned task can be answered, edited, diagnosed, or validated quickly in one bounded run, and should stop with reason when complexity or cost breaks that boundary." tools: ["read", "edit", "search", "execute", "web"] disable-model-invocation: true agents: [] -handoffs: - - label: "Next step: Reopen planning" - agent: "internal-gateway-idea-brainstorming" - prompt: "Reopen planning because this task no longer fits the simple single-lane fast path. Preserve scope, validation, and risk, then choose the right retained-plan profile before execution." - send: false - - label: "Next step: Pressure-test task" - agent: "internal-gateway-critical-master" - prompt: "Pressure-test the reasoning, assumptions, or failure modes that made this task leave the simple fast path." - send: false - - label: "Next step: Review target" - agent: "internal-gateway-review" - prompt: "The work above became defect-first analysis rather than direct execution. Review the concrete target and stop before applying fixes." - send: false --- # Internal Gateway Simple Task diff --git a/.github/skills/internal-gateway-simple-task/SKILL.md b/.github/skills/internal-gateway-simple-task/SKILL.md index 9e843598..4ae30518 100644 --- a/.github/skills/internal-gateway-simple-task/SKILL.md +++ b/.github/skills/internal-gateway-simple-task/SKILL.md @@ -1,265 +1,155 @@ --- name: internal-gateway-simple-task -description: Use when a concrete low-to-medium-risk repository-owned coding or non-coding task can be answered, edited, diagnosed, validated, or planned quickly through one lane. +description: Use when a concrete low-to-medium-risk repository-owned coding or non-coding task can be completed quickly in one bounded run. --- # Internal Gateway Simple Task ## Referenced skills -Load these skills by name only when the active lane proves the narrower owner. -Treat this list as an on-demand index, not a preload bundle. - -- `grill-me`: mandatory pre-action interview for non-trivial simple work and one focused clarification block for simple blockers. -- `internal-gateway-idea-brainstorming`: planning owner when simple work no longer fits or needs substantive ideation. -- `internal-gateway-writing-plans`: retained-plan authoring owner when simple task switches to plan mode. -- `internal-gateway-review`: review owner when defect-first analysis becomes dominant. -- `internal-gateway-critical-master`: mandatory post-interview critical gate for non-trivial simple work and critical owner when assumptions or failure modes dominate. -- `superpowers-verification-before-completion`: final evidence gate. - -Use this skill as the skill-first fast path for concrete repository-owned work. -It is single-lane and single-phase by design, but it can switch to **plan mode** -to produce a retained plan instead of executing when the user asks for a plan or -when same-chat execution would be less economical than a retained plan. - -When execution stays in the same chat, this skill owns end-to-end direct -completion control. It must keep the original request, emerged requirements, -mandatory applicable requirements, validation path, and evidence status aligned -until everything in scope is closed or a blocker is explicit. - -Before operational work, produce a lean Readiness Brief and name the gate -outcome. Stop for explicit user approval unless a narrower loaded skill defines -a deterministic auto-execute lane with its own zero-blocker, no-drift, and -post-run reporting gates. - -`references/support-routing.md` remains the single source of truth for claim-gate owners in simple mode. -`references/simple-lanes.md` remains the single source of truth for lane definitions, support posture, and validation per lane. -`references/plan-mode.md` remains the single source of truth for plan-mode activation, confirmation, and delegation to `internal-gateway-writing-plans`. - -Use `scripts/resolve_simple_task.py` when the task facts are already known and -the bundle only needs a deterministic gate or claim-gate answer. Use -`scripts/suggest_support_skills.py` only when paths or symptoms are known and -support selection is still noisy. -Do not preload adjacent skill bodies speculatively. Load only the active lane -owner, mandatory gate owners, and the smallest support owner proven by the -evidence. +- `grill-me`: compact Gate 1 interview after local preflight and Initial Idea Ordering. +- `internal-gateway-critical-master`: Gate 2 challenge before non-trivial action. +- `superpowers-verification-before-completion`: final evidence gate before completion, readiness, passing, fixed, or no-gap claims. + +Do not name other skills, agents, or workflow owners from this bundle; when stopping, explain the violated condition and let the user choose the next path. + +Use this skill as the fast path for concrete bounded work that should finish in the current run. It should answer, edit, diagnose, or validate end-to-end by default when the target, anti-scope, and validation path are concrete enough to execute safely. + +Stop only when the work becomes materially complex, too costly for the current run, ambiguous, unsafe, multi-phase, approval-bound, or not locally verifiable. When stopping, report the exact boundary break instead of delegating by name. + +Use local references and scripts only to keep the decision process compact and deterministic. Keep the working state small, prefer bounded evidence, and preserve direct completion ownership inside this bundle. ## When to use -- The outcome, target, command, or validation path is already concrete. -- One quick lane can finish: `answer`, `edit`, `diagnose`, `validate`, or `plan`. -- The user explicitly asks for a plan, or cost signals show that a retained plan is cheaper than same-chat execution. +- The task is concrete and repository-owned. +- The target path, requested outcome, or validation path is already known or can be recovered with one focused clarification. +- One bounded run can answer, edit, diagnose, or validate the work without staged workflow changes. ## When not to use -- Ownership, rollout, governance, or cross-boundary tradeoffs still need a decision; use `internal-gateway-idea-brainstorming`. -- The request is defect-first review; use `internal-gateway-review`. -- The request is approved retained-plan execution; use `internal-gateway-execute-plans`. -- The primary request is pressure testing; use `internal-gateway-critical-master`. - -## Simple Gate Policy - -Classify every simple task before operational work as `full-gate`, -`trivial-skip`, `escalate`, or `plan-mode`. - -- Use `full-gate` by default unless the task is proven trivial and venial. -- For concrete low-to-medium-risk non-trivial work, the required `full-gate` - posture is `compact full-gate`: one compact `grill-me` block, one critical - outcome, one lean Readiness Brief, then explicit user approval. -- Run `grill-me` first with one compact numbered block, then the critical gate. -- Before editing, executing, or finalizing, ask the user to respond first to - the `grill-me` block and then to the critical outcome. -- Treat `full`, `idea`, and `complete` as depth keywords. A depth keyword - forbids `trivial-skip`. Run `grill-me` first, then the critical gate. -- Use `trivial-skip` only for a local answer, tiny edit, focused read, or - validator run with no material ambiguity or material risk and with obvious - validation or a named validation gap. -- When using `trivial-skip`, emit a short Trivial-skip proof before operational - work. -- Use `plan-mode` when the user explicitly asks for a plan or when cost signals - show that same-chat execution is less economical than a retained plan. - See `references/plan-mode.md` for the exact cost-signal checklist. -- If planning, review, critical pressure, or multi-phase validation becomes the - real job, `escalate`. - -### Token Budget Gate - -- Run a `Token Budget Gate` before choosing `trivial-skip` or `plan-mode` when - the user asks for low-token execution or the task centers on large tabular - files, log exports, repeated tool output, or broad file changes. -- Before that gate, do not launch broad scans, repo-wide discovery, full log - reads, wide dependency walks, or wide workbook and export inspection when the - task touches workbooks, exports, logs, dependency trees, or tabular data. -- For Copilot or debug log analysis, start with file size, model-call counts, - prompt or token aggregates, tool-span counts, result-size summaries, and - targeted slices; avoid full JSON dumps or prompt bodies unless they are the - missing evidence. -- Keep compact reporting runner-agnostic: ask for bounded summaries, exit - state, counts, anomalies, and evidence gaps, but do not require `jq`, `awk`, - shell flags, or terminal-only recipes unless they are already the local - workflow being analyzed. -- A cost checkpoint pauses before a new expensive tool burst, broad reread, or - repeated execution loop. It does not interrupt ordinary conversation, - grill-me questioning, or collaborative reasoning when no expensive tool - action is being launched. -- If the user explicitly asks for full output, deeper slices, or continued - execution, name the likely token or context impact before expanding and then - either proceed with the smallest bounded next slice or ask for confirmation - before the new expensive burst. -- Keep `trivial-skip` only for truly tiny local work with obvious validation - and no material completeness risk. -- If context pressure could hide required validation, data integrity, or route - ownership, prefer `plan-mode` and delegate retained writing instead of - stretching same-chat execution. +- The request is primarily brainstorming, architecture selection, or tradeoff design. +- The request is primarily defect-first review. +- The task needs a retained plan, a multi-phase rollout, or separate execution approval. +- The task cannot be validated locally or safely with bounded evidence. + +## Autonomous Completion Rule + +Complete the task in the current run when it is concrete, bounded, and locally validatable. Do not stop merely because a supporting method exists. + +When the task cannot safely continue, stop with these fields: + +- `why stopped` +- `violated condition` +- `user decision needed` +- `evidence required before continuing` + +Stop output must not name another skill, agent, or owner. + +## Initial Idea Ordering + +Complete this local ordering before `grill-me` for any non-trivial task: + +- `original request` +- `emerged requirements` +- `actual problem` +- `proposed direction` +- `hidden assumption` +- `smaller move` +- `alternative path` +- `validation signal` +- `stop signal` + +This ordering is local to this skill. Do not import it from any other bundle. + +## Gate Policy + +Classify the task before operational work as `trivial-skip`, `full-gate`, or `stop-with-reason`. + +- Use `trivial-skip` only for a local answer, tiny edit, focused read, or validator run with no material ambiguity, no material risk, and an obvious validation path or explicit validation gap. +- Use `full-gate` for everything else that still fits same-run completion. +- Use `stop-with-reason` when the work becomes too complex, too costly, ambiguous, unsafe, approval-bound, multi-phase, or likely incomplete in the current run. + +Gate order: + +1. Bounded evidence. +2. Complexity and cost gate. +3. Initial Idea Ordering. +4. `grill-me` when needed. +5. Critical challenge. +6. Readiness Brief. +7. Execution. +8. Validation. +9. Final evidence gate. + +Depth keywords such as `full`, `idea`, and `complete` forbid `trivial-skip`. + +## Readiness Brief + +Before operational work, produce a short local brief with no placeholders: + +- `Task` +- `Goal` +- `Scope` +- `Anti-scope` +- `Files expected` +- `Approach` +- `Executable behavior` +- `Validation path` +- `Main risk` +- `Stop conditions` +- `Approval` + +This brief must stay shorter than a retained plan. ## Simple Procedure -1. Inspect local files first. -2. Preserve compact working state: avoid full-context rereads unless new evidence invalidates the active lane assumptions. -3. Detect depth keywords: `full`, `idea`, or `complete`. -4. Classify the gate outcome as `full-gate`, `trivial-skip`, `escalate`, or - `plan-mode`. -5. For `full-gate`, load `grill-me`, ask one compact numbered block, then load - `internal-gateway-critical-master` after the user's response. -6. For `trivial-skip`, emit the Trivial-skip proof before operational work. -7. For `plan-mode`, run `grill-me` and `internal-gateway-critical-master` as for - `full-gate`, then load `internal-gateway-writing-plans` and delegate retained - writing. Stop before execution. -8. Confirm the task still fits one quick lane and choose that lane from - `references/simple-lanes.md`. -9. Select only directly applicable skill owners and required references from - prompt, target path, runtime, ownership, and validation path. Do not - preload full adjacent skills without evidence. -10. Build a Readiness Brief before operational work: task, lane-owner, primary - assumption or risk, focused validation path, gate outcome, and explicit - confirmation prompt or named auto-execute exception. -11. Use `scripts/resolve_simple_task.py gate` when the facts are already known - and the bundle only needs a deterministic gate and readiness summary. -12. Stop and wait for explicit user approval before executing the lane unless - the selected narrower owner declares a deterministic auto-execute lane with - its own zero-blocker, zero ambiguous drift, and no destructive or - reverse-direction action. -13. After approval, move directly into the edit or validation loop. Keep - intermediary prose to blockers, risk changes, and evidence. -14. Identify mandatory applicable requirements and direct source items - internally before execution; do not emit a default user checklist. -15. Maintain `Direct Execution Control` for non-plan execution. -16. Execute the one concrete lane with the Agentic Execution Loop, or, in - `plan-mode`, delegate retained writing and stop before execution. -17. Run focused validation or name the explicit gap. -18. Run a pre-close compliance audit over mandatory applicable requirements - only. Delegate fresh-evidence mechanics to - `superpowers-verification-before-completion`. -19. Run `Direct Completion Control` before any `DONE`, readiness, fixed, or - complete claim. -20. Block completion claims when mandatory applicable requirements remain - unverified. -21. If architecture ownership, owner conflicts, or validation strategy are - ambiguous, escalate instead of assuming a universal rule. -22. If the task stops being simple, stop and issue an escalation alert. - -Escalation trigger: if evidence collection, ownership checks, or validation needs spill into multi-phase execution, route to the narrow next owner instead of expanding the fast path. - -## Agentic Execution Loop - -When execution is already authorized, stay inside the active owner, target -scope, anti-scope, and validation path, then iterate: - -1. Confirm the current goal, nearest evidence, and the open direct-control item. +1. Inspect the nearest local evidence first. +2. Decide whether the task is `trivial-skip`, `full-gate`, or `stop-with-reason`. +3. For non-trivial work, complete Initial Idea Ordering before `grill-me`. +4. Ask one compact `grill-me` block only when it is needed to continue the active lane. +5. Run the critical challenge before non-trivial action. +6. Build the Readiness Brief. +7. Execute the smallest coherent in-scope move. +8. Run focused validation or report the exact validation gap. +9. Use the final evidence gate before positive claims. + +## Execution Loop + +When execution is authorized, iterate with the smallest useful cycle: + +1. Confirm the current goal, scope boundary, and next check. 2. Apply the smallest in-scope action. 3. Run the focused validation or evidence check. -4. If validation fails for an in-scope, repairable reason, fix once and re-run. -5. Continue only while evidence improves and no stop condition fires. -6. Stop with `DONE` only when direct-control coverage is closed, or stop with a - blocker or an explicit evidence gap. - -Stop on scope drift, destructive action, owner conflict, missing validation -path, human approval need, secret exposure risk, or repeated non-improving -failures. - -## Direct Execution Control - -Use this mode when the task executes directly in the same chat instead of -creating a retained plan. Keep the control state compact and internal unless a -gap, blocker, or user-facing status update needs to expose it. - -Track these fields for non-trivial direct execution: - -- original intent, separated from emerged requirements -- target, anti-scope, owner, lane-change owner, and validation path -- direct source items with observable acceptance, evidence class, status, and - route through the active lane or explicit non-action -- mandatory applicable requirements from selected skills and local evidence -- stop conditions, blockers, and validation gaps - -During execution, update direct-control status after every edit, command, -validator, or explicit non-action. If new in-scope work is discovered, add it -before continuing. If new work changes owner, scope, or validation strategy, -stop and escalate instead of silently shrinking the task. - -### Direct Completion Control - -Before any `DONE`, fixed, complete, ready, validator-passes, or no-gap claim: - -1. Compare the original intent, emerged requirements, direct source items, - current diff or file state, validation output, and explicit non-actions. -2. Confirm every in-scope source item is closed with observable evidence or a - named explicit non-action. -3. Confirm mandatory applicable requirements are verified with fresh evidence, - or the exact evidence gap is named. -4. Confirm no stop condition remains open and no sensitive value was hardcoded - when the touched surface could expose secrets or tenant data. -5. Continue the Agentic Execution Loop when safe work remains; otherwise stop - with a blocker or explicit evidence gap. - -One successful validator, one patched file, or one satisfied subtask is not -enough for completion unless direct-control coverage shows that all in-scope -items are closed. - -## Plan Mode - -Plan mode keeps a concrete simple task single-lane and single-phase while -writing a retained plan instead of executing. `references/plan-mode.md` owns the -detailed activation, Token Budget Gate, confirmation, delegation, boundary, -and example rules. After `grill-me` and critical review, load -`internal-gateway-writing-plans` for retained writing and hand off future -execution to `internal-gateway-execute-plans`. +4. Repair once when the failure is still in scope and improving. +5. Continue only while evidence improves. +6. Stop with reason when risk, cost, ambiguity, or validation failure crosses the boundary. + +## Generic Executable Behavior Rule + +When executable behavior changes, use a generic test-first loop: + +1. Identify the observable behavior. +2. Choose the smallest useful stable check. +3. Make it fail first when practical. +4. Implement the minimum change. +5. Re-run the focused check. +6. Refactor only after passing. +7. Finish with the closest broader validation. + +If no useful seam exists, state the seam gap explicitly. ## Deterministic Helpers -- `scripts/resolve_simple_task.py gate`: returns `full-gate`, - `trivial-skip`, `escalate`, or `plan-mode` plus a lean Readiness Brief from - normalized task facts. -- `scripts/resolve_simple_task.py claim`: maps status claims to the required - owners and evidence gates before the final answer. -- `scripts/suggest_support_skills.py`: returns path and symptom-based support - hints. It is advisory and does not override repository evidence. +- `scripts/resolve_simple_task.py gate`: returns a deterministic gate outcome plus a local Readiness Brief. +- `scripts/resolve_simple_task.py claim`: returns evidence requirements for strong status claims. +- `scripts/suggest_support_skills.py`: returns generic method hints when the next move is still noisy. ## Validation -- Work stayed single-lane and single-phase, or escalation was explicit. -- `full-gate`, `trivial-skip`, `escalate`, or `plan-mode` was named before operational work. -- Non-trivial simple work used `grill-me` before `internal-gateway-critical-master`, and depth keywords prevented `trivial-skip`. -- `trivial-skip` included evidence that the task was trivial and venial. -- Readiness Brief stayed lean, named the lane-owner and validation path, and - included an explicit approval checkpoint or named the narrower auto-execute exception. -- After approval, execution moved quickly from the chosen edit or validation - lane to focused verification without extra prose bursts. -- Focused validation ran before completion claims, or the exact validation gap was reported. -- Direct non-plan execution used `Direct Execution Control` for non-trivial - work and `Direct Completion Control` before completion claims. -- The Agentic Execution Loop stayed inside the authorized owner, scope, and - validation path. -- Auto-execute exceptions stopped on blockers, ambiguous drift, destructive actions, reverse-direction writes, or missing validation evidence. -- Completion claims were blocked when mandatory applicable requirements were still unverified. -- Completion claims were blocked when any in-scope direct source item remained - open, unverified, or silently dropped. -- Output stayed concise unless a gap, exception, or escalation had to be reported. - -## Common failure modes - -- Treating loaded skills as automatically mandatory. -- Skipping `grill-me`, the critical gate, a Trivial-skip proof, or approval. -- Treating depth keywords, implicit `plan-mode`, broad scans, or - `next_action.allowed=true` as harmless. -- Declaring completion while mandatory evidence, direct source items, emerged in-scope requirements, or ownership proof remain open. +- Only the three retained skill names appear in this bundle. +- Initial Idea Ordering is completed before `grill-me` for non-trivial work. +- The critical challenge runs before non-trivial action. +- Concrete bounded work completes in the same run unless `stop-with-reason` is explicit. +- Stop output explains the exact violated condition and required evidence. +- Executable behavior changes follow the generic test-first loop when a useful seam exists. +- Positive claims rely on fresh evidence before completion. diff --git a/.github/skills/internal-gateway-simple-task/agents/openai.yaml b/.github/skills/internal-gateway-simple-task/agents/openai.yaml index dd44cef2..cf307d73 100644 --- a/.github/skills/internal-gateway-simple-task/agents/openai.yaml +++ b/.github/skills/internal-gateway-simple-task/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Internal Gateway Simple Task" short_description: "Fast path for concrete repository tasks" - default_prompt: "Use $internal-gateway-simple-task for a concrete coding or non-coding task that needs quick support-skill selection, direct execution, diagnosis, validation, or plan authoring. For non-plan execution, maintain Direct Execution Control and do not claim DONE, fixed, ready, complete, or validator-passes until all in-scope source items and mandatory applicable requirements are closed with fresh evidence or an explicit gap. Switch to plan mode when the user asks for a plan or when same-chat execution would be less economical than a retained plan, delegating retained writing to internal-gateway-writing-plans instead of executing. Route approved retained-plan execution to internal-gateway-execute-plans." + default_prompt: "Use $internal-gateway-simple-task for a concrete coding or non-coding task that should finish end-to-end in one bounded run. Start with bounded evidence, local idea ordering, and one compact clarification only when necessary. Use `grill-me` when the task is non-trivial, run `internal-gateway-critical-master` before non-trivial action, stop with reason when the work becomes too large, costly, ambiguous, unsafe, or not locally verifiable, and use `superpowers-verification-before-completion` before positive claims." diff --git a/.github/skills/internal-gateway-simple-task/references/clarification-gate.md b/.github/skills/internal-gateway-simple-task/references/clarification-gate.md index cf2b7e7b..2ede6696 100644 --- a/.github/skills/internal-gateway-simple-task/references/clarification-gate.md +++ b/.github/skills/internal-gateway-simple-task/references/clarification-gate.md @@ -1,86 +1,64 @@ # Simple Task Clarification Gate -Use this reference when simple mode must decide whether to run the full -interview and critical gate, prove a trivial skip, or ask one focused blocker -question before a quick lane can continue. +Use this reference when the simple-task bundle must decide whether one focused clarification can keep the current lane moving. ## Core Rule -Simple mode defaults to a full gate for non-trivial work: ask one compact focused `grill-me` block before operational work, then run -`internal-gateway-critical-master` after the user's interview response. Ask the -user to respond first to `grill-me` and then to the critical outcome. +Default to the full gate for non-trivial work: bounded evidence, Initial Idea Ordering, one compact `grill-me` block when needed, critical challenge, then the local Readiness Brief. -Skip both gates only with a Trivial-skip proof. The proof must show the task is -trivial and venial, no depth keyword is present, and the focused validation path -or gap is already clear. +Skip the gate only with a Trivial-skip proof showing: -If the missing answer would decide ownership, rollout, governance, tradeoffs, -validation strategy, or whether the work needs a plan, stop simple mode and -escalate to `internal-gateway-idea-brainstorming` or `internal-gateway-review`. +- the task is tiny and local +- no depth keyword is present +- the validation path is obvious or the validation gap is explicit -If the clarification or full gate reveals material risk, hidden assumptions, or -dominant failure-mode pressure, escalate to `internal-gateway-critical-master`. - -## Depth Keyword Override - -Treat `full`, `idea`, and `complete` as user-forced full-gate keywords when the -request still targets simple mode. Do not use `trivial-skip` after one of these -keywords. Run `grill-me` first, then the critical gate, unless the keyword proves -the task belongs to a narrower planning, review, or pressure-testing owner. +If a missing answer would change scope, validation, cost, risk, or target state, stop with reason instead of continuing. ## Exit Check -Run this check before skipping or calling `grill-me`: +Run this check before asking a clarification: -1. Did the user provide a depth keyword: `full`, `idea`, or `complete`? -2. Is the task more than a local answer, tiny edit, focused read, or validator - run with obvious validation? -3. Does the prompt actually need a plan, retained plan, plan rewrite, review, - or clarify-first workflow? -4. Would the missing answer change the owner, target state, anti-scope, - validation path, or rollout posture? -5. Would more than one dependent clarification block be needed? -6. Did the user explicitly ask for a plan, or do cost signals show that - same-chat execution is less economical than a retained plan? +1. Did the user force a deeper posture with `full`, `idea`, or `complete`? +2. Is the task more than a local answer, tiny edit, focused read, or validator run? +3. Would the missing answer change scope, anti-scope, validation path, or approval boundary? +4. Would more than one dependent clarification block be needed? +5. Would the answer turn the task into multi-phase, costly, or unsafe work? -If answer 1 or 2 is yes and answers 3-5 are no, use the full gate. If answer 6 -is yes and the task is still concrete, classify `plan-mode` and ask for -confirmation when the trigger is implicit. If any of answers 3-5 is yes and the -task is not a concrete plan-mode candidate, do not continue in simple mode. +If `1` or `2` is yes and `3-5` are no, use the full gate. If any of `3-5` is yes, stop with reason. ## Single Clarification Limit -Use one compact `grill-me` block for the full gate or when missing user intent, -target path, input data, local context, or a blocker prevents starting or -continuing the active simple lane. Ask only for the minimum context needed to -resume the current lane. - -If the first clarification answer creates another dependent question set, or if -the blocker still cannot be resolved inside the same focused block, escalate. - -## grill-me Boundary - -`grill-me` in simple mode may recover: +Ask at most one compact `grill-me` block for: - missing file, path, or artifact target - missing input data or reproduction step - missing local context needed to start the chosen lane -- one blocker that prevents continuing an already valid simple lane +- one blocker preventing an otherwise valid simple lane + +If that answer creates another dependent question set, stop with reason. + +## `grill-me` Boundary + +`grill-me` may recover: + +- a missing path or artifact +- a missing reproduction step +- one bounded blocker +- one missing local fact needed to execute -`grill-me` in simple mode must not decide: +`grill-me` must not decide: -- ownership, lane, or phase changes that need staged workflow -- rollout posture, governance, or approval boundaries -- design tradeoffs, architecture, or cross-boundary scope -- validation strategy beyond the nearest focused check +- staged workflow changes +- architecture or design tradeoffs +- rollout posture +- approval boundaries +- broad validation strategy redesign -## Escalation Triggers +## Stop Conditions -Escalate to `internal-gateway-idea-brainstorming`, -`internal-gateway-review`, or `internal-gateway-critical-master` when: +Stop with reason when: - the exit check fails - the clarification limit is exceeded -- the answer would change simple-mode ownership or anti-scope -- the task now needs retained-plan, review, or critical-challenge handling -- material risk or failure-mode pressure dominates +- the answer would materially change scope or validation +- the work becomes multi-phase, costly, ambiguous, or unsafe diff --git a/.github/skills/internal-gateway-simple-task/references/plan-mode.md b/.github/skills/internal-gateway-simple-task/references/plan-mode.md index b5899cb5..1c5813e3 100644 --- a/.github/skills/internal-gateway-simple-task/references/plan-mode.md +++ b/.github/skills/internal-gateway-simple-task/references/plan-mode.md @@ -1,137 +1,49 @@ -# Simple Task Plan Mode +# Simple Task Plan Recommendation -Use this reference when `internal-gateway-simple-task` is about to classify a -concrete task as `plan-mode` instead of executing it in the same chat. +Use this reference when same-run execution should stop and a retained plan should be recommended instead. -## What plan mode is +## Core Rule -Plan mode keeps the task inside `internal-gateway-simple-task` but changes the -output shape: the agent writes a retained plan and stops before execution. The -task must remain concrete and single-lane; only the delivery mode changes. +This bundle does not write retained plans. It detects when same-run execution is no longer economical or safe, explains why, and stops so the user can choose whether to create a plan. -## Activation +## Triggers -### Mandatory explicit trigger +Recommend a plan when one or more of these signals is present: -If the user asks for a plan with any of these signals, switch to plan mode and -honor the request without trying to execute: +- the user explicitly asks for a plan +- the task needs more than roughly 5-7 concrete executable steps +- the task touches more than roughly 3 unrelated files or path families +- the task has multiple independent validators +- the task depends on external approvals, credentials, or third-party state +- the task risks context pressure before validation can complete +- the work centers on large exports, tables, logs, or broad mechanical change -- `plan`, `piano`, `modalità plan`, `retained plan` -- `scrivi il piano`, `scrivi un plan`, `fammi il piano` -- `non eseguire ancora`, `stop prima dell'esecuzione` -- `salva il piano`, `persistilo`, `plan mode` +## Procedure -### Implicit cost-signal trigger +1. Classify the cost or complexity signal. +2. Explain why same-run execution is unsafe or uneconomical. +3. Ask the user whether to create a retained plan. +4. Stop without writing one. -If the user does not mention a plan, but the following signals show that -same-chat execution would be less economical than a retained plan, declare the -signal and ask for explicit confirmation before switching to plan mode. +## Token Budget Gate -### Cost-signal checklist +When the main signal is context pressure, keep evidence compact before stopping: -- The user hints at continuation across chats, e.g. *"continuiamo dopo"*, - *"in un'altra chat"*, *"domani"*, *"salva per dopo"*, *"persistilo"*. -- The task requires more than roughly 5-7 concrete executable steps. -- The task touches more than roughly 3 unrelated files or path families. -- The task has multiple independent validators or validation surfaces. -- The task depends on external pins that may not resolve in this chat - (tokens, API availability, runner state, human approval, third-party - provisioning). -- There is material risk that context pressure or chat limits will interrupt - the work before it can be validated. -- The task centers on large `.csv`, `.tsv`, `.xlsx`, JSON log exports, - repeated tool output, or broad file changes that would bloat chat context. -- The user is asking for a large refactor, migration, or cross-file mechanical - change that is safer as a tracked plan. +- prefer bounded summaries over raw dumps +- avoid broad scans and repeated output bursts +- name the likely context impact before any expensive expansion -Do not switch to plan mode implicitly without declaring the detected signals -and asking for user confirmation. +## Stop Output -### Token Budget Gate +When recommending a plan, report: -When the cost signals come mainly from context pressure instead of task count, -prefer a compact evidence posture rather than a raw-output posture. Keep same-chat -execution only for tiny local work; otherwise switch to `plan-mode` and let -`internal-gateway-writing-plans` handle retained-writing decisions. - -A cost checkpoint pauses before a new expensive tool burst, broad reread, or -multi-step execution loop. It does not interrupt ordinary conversation, -grill-me analysis, or collaborative study when no expensive tool action is -starting. - -When the user explicitly asks for broader output, deeper analysis, or continued -execution, name the likely token or context impact first and then either -continue with the smallest bounded next slice or ask for confirmation before -the new expensive burst. - -## Delegated retained writing - -`internal-gateway-simple-task` does not choose retained-plan profile, folder -shape, or artifact internals. It only decides that same-chat execution should -stop and retained writing should begin. - -When plan mode is confirmed, pass these facts to `internal-gateway-writing-plans`: - -- concrete target state -- anti-scope -- nearest owner -- validation path or explicit validation gap -- cost signals that made same-chat execution less economical -- stop conditions -- observable acceptance - -`internal-gateway-writing-plans` then delegates artifact decisions to -`superpowers-writing-plans`. - -## Confirmation rule for implicit triggers - -For implicit cost-signal triggers, emit a short statement that: - -1. Names the detected cost signals. -2. Proposes `plan-mode` and names that retained writing will be delegated to - `internal-gateway-writing-plans`. -3. Asks the user to confirm or decline the switch before writing anything. - -Do not write the retained plan until the user confirms. - -## Procedure inside plan mode - -1. Classify the gate as `plan-mode`. -2. Run `grill-me` with one compact numbered block. -3. Load `internal-gateway-critical-master` after the user responds. -4. If critical gate returns confident, load `internal-gateway-writing-plans`. -5. Pass the preflight facts to `internal-gateway-writing-plans` and let it - delegate the writing outcome. -6. Stop before execution. Report the plan folder and hand off to - `internal-gateway-execute-plans` for future execution. +- `why stopped` +- `cost or complexity signal` +- `user decision needed` +- `evidence required before execution can continue` ## Boundaries -- Do not use plan mode to avoid ownership ambiguity. If the target, anti-scope, - or validation strategy is unclear, `escalate` to `internal-gateway-idea-brainstorming` - or `internal-gateway-review`. -- Do not use plan mode for vague ideas or substantive tradeoffs; those belong - to `internal-gateway-idea-brainstorming`. -- Do not execute the plan inside `internal-gateway-simple-task`. Execution is - owned by `internal-gateway-execute-plans`. -- Do not silently downgrade an explicit user request for a plan into a - same-chat execution. - -## Examples - -- User: *"Aggiungi un nuovo campo al modello e al database"* with 8 files and - a migration: classify `plan-mode` implicit, ask confirmation, then write a - retained plan through `internal-gateway-writing-plans`. -- User: *"Modalità plan: riscrivi il parser dei skills"*: classify - `plan-mode` explicit, run the full gate, write the plan, stop. -- User: *"Cosa ne pensi di rifattorizzare tutto?"*: this is not concrete; do - not use plan mode. Escalate to `internal-gateway-idea-brainstorming`. - -## Common failure modes - -- Treating an implicit cost signal as a decision to write a plan without - confirming with the user. -- Reintroducing retained-plan profile decisions instead of delegating them. -- Writing the plan but then executing it inside simple task. -- Using plan mode as a way to avoid saying that the task is actually vague or - ownership-ambiguous. +- Do not use plan recommendation to hide ambiguity about the target state. +- Do not continue same-run execution after deciding that a plan is needed. +- Do not write the retained plan from this bundle. diff --git a/.github/skills/internal-gateway-simple-task/references/simple-lanes.md b/.github/skills/internal-gateway-simple-task/references/simple-lanes.md index 71f0052e..b9a475a6 100644 --- a/.github/skills/internal-gateway-simple-task/references/simple-lanes.md +++ b/.github/skills/internal-gateway-simple-task/references/simple-lanes.md @@ -1,85 +1,56 @@ # Simple Task Lanes -Use this reference when `SKILL.md` confirms the task is simple, but the active -lane or output shape still needs a quick decision. +Use this reference when the task stays simple but the execution shape still needs a quick decision. ## Lane Selection -| Lane | Use when | Support posture | Validation | +| Lane | Use when | Method posture | Validation | | --- | --- | --- | --- | -| `answer` | The user needs a direct explanation, decision, or repository-context answer. | Load only the relevant domain owner when facts depend on it. | Cite inspected files or state the evidence gap. | -| `edit` | The target file and desired outcome are clear. | Load the smallest file-type, runtime, authoring, or domain owner proved by evidence. | Run the closest validator, test, lint, syntax check, or focused manual check. | -| `diagnose` | A failure, bug, drift, or unexpected behavior is present. | Reproduce the loop first, then add runtime or domain support only if needed. | The original loop must pass after the fix, or the blocker must be explicit. | -| `validate` | The main job is checking an existing artifact, command, or result. | Load the owner for the validation surface only when needed. | Report the exact check, result, and remaining gap. | -| `plan` | The task is concrete but the user asks for a plan, or cost signals show that same-chat execution is less economical than a retained plan. | Load `internal-gateway-writing-plans` after `grill-me` and `internal-gateway-critical-master`. | The plan is written, validated against the writing contract, and the folder is ready for `internal-gateway-execute-plans`. | -| `escalate` | The task becomes staged, review-owned, retained-plan-owned, critical-challenge owned, or exceeds `references/clarification-gate.md`. | Stop and name the next owner. | Provide boundary break, owner, scope, action, validation path, and risk. | - -## Examples - -- Clear prose or skill wording fix: inspect the paired asset and relevant - domain owner, edit the smallest owner, then run the closest skill or Markdown - validation. -- Small code or script change: inspect local patterns, load the matching runtime - support only if needed, edit, then run the focused executable check. -- Known failure: reproduce the failing loop, fix the root cause, then rerun that - loop before claiming completion. -- Advisory answer: inspect repository evidence first, answer in chat, and name - what was not validated if no command was run. -- Ownership ambiguity: stop at `escalate` instead of converting uncertainty into - hidden planning. -- Plan mode: concrete task that is too large for same-chat execution or - explicitly requested as a plan; classify `plan-mode`, delegate retained - writing to `internal-gateway-writing-plans`, and stop before execution. -- Clarification overflow: use `escalate` when the simple clarification gate - would need more than one focused `grill-me` block. +| `answer` | The user needs a direct explanation, decision, or repository-context answer. | Inspect the nearest evidence and answer directly. | Cite the inspected evidence or state the gap. | +| `edit` | The target file and desired outcome are clear. | Make the smallest coherent change. | Run the closest validator, test, syntax check, or bounded manual check. | +| `diagnose` | A bug, failure, drift, or unexpected behavior is present. | Reproduce first, then test one falsifiable hypothesis at a time. | The original loop must pass after the fix, or the blocker must be explicit. | +| `validate` | The main job is checking an existing artifact, command, or result. | Stay read-first and report exact outcomes. | Name the exact check, result, and remaining gap. | + +If cost or complexity exceeds same-run execution, stop and recommend a retained plan instead of stretching the lane. If the boundary break is broader than that, stop with reason. ## Output Shapes For `answer`, return the answer, evidence, and uncertainty. -For `edit`, return `lane`, `support-loaded`, `files-touched`, direct-control -status, focused validation, and residual risk. +For `edit`, return `lane`, `files-touched`, validation, and residual risk. + +For `diagnose`, return `lane`, reproduced failure, root cause, fix or blocker, and evidence. -For `diagnose`, return `lane`, `support-loaded`, the reproduced failure, root -cause, direct-control status, fix or blocker, and evidence. +For `validate`, return `lane`, check, result, and any follow-up gap. -For `validate`, return `lane`, `support-loaded`, the command or check, result, -direct-control status, and any follow-up owner. +For `stop-with-reason`, return: -For `escalate`, return the boundary break, next owner, scope, action, validation -path, and risk. +- `boundary break` +- `why stopped` +- `user decision needed` +- `evidence required` ## Multi-Source Mismatch Procedure -When the same symptom may come from multiple sources or transformation layers, -use this lightweight procedure before patching: +When the same symptom may come from multiple sources or transformation layers: -1. Name the target artifact or observed mismatch. -2. List the candidate sources of truth and mark one authoritative only when - evidence already proves it. -3. Map the transformation layers between source and target. -4. Choose the cheapest check that can falsify one source or layer at a time. -5. Stop expanding once one layer is proven wrong or source authority remains - unproven and needs escalation. +1. Name the target artifact or mismatch. +2. List candidate sources of truth. +3. Map the transformation layers. +4. Choose the cheapest check that can falsify one layer at a time. +5. Stop expanding once one failing layer is proven or source authority stays unclear. -Patch only the proven failing layer. Do not convert this generic procedure into -domain-specific incident language. +Patch only the proven failing layer. ## Diagnose Deterministic Failure Procedure -When a failure report already contains exact test names, file paths, symbols, or -expected strings, use this ordered procedure before broader evidence gathering: +When a failure report already contains exact test names, file paths, symbols, or expected strings: -1. Extract exact anchors from the failure output. -2. Search those exact anchors in the repository. -3. Read only nearby controlling context around the matches. -4. State one falsifiable hypothesis and the cheapest check that can disprove it. -5. Verify the exact context of every hunk you intend to edit. +1. Extract the exact anchors. +2. Search those anchors. +3. Read only nearby controlling context. +4. State one falsifiable hypothesis and the cheapest check. +5. Verify the exact context of each intended edit. 6. Apply the smallest grounded edit. 7. Immediately run focused validation. -8. Expand evidence only when the check falsifies the hypothesis or leaves material - ambiguity. - -References and neighboring owners load only when exact evidence is insufficient. -Redundant or speculative edits must not appear in the first patch. The original -failure loop remains required before any fixed or resolved claim. +8. Expand only if the check falsifies the hypothesis or leaves material ambiguity. diff --git a/.github/skills/internal-gateway-simple-task/references/support-routing.md b/.github/skills/internal-gateway-simple-task/references/support-routing.md index 7c8f8f41..f883e197 100644 --- a/.github/skills/internal-gateway-simple-task/references/support-routing.md +++ b/.github/skills/internal-gateway-simple-task/references/support-routing.md @@ -1,93 +1,54 @@ -# Simple Task Support Routing +# Simple Task Support Methods -Use this reference after the quick lane is selected and support ownership is -still noisy. Keep support conditional and minimal. +Use this reference when the next move is still noisy after lane selection. Keep the guidance generic and evidence-first. ## Core Rule -Load support for the first real blocker, file type, runtime, domain signal, or -validation path. Do not load a support bundle because it might become useful. -Treat a support skill's `## Referenced skills` section as an owner index, not a -reason to preload its neighbors. Follow those references only when current -evidence proves the narrower owner. -When a path helper is used, prefer the single narrowest owner proved by that -path. Do not emit a base-plus-depth stack or a referenced-skill chain for a -simple edit unless the path itself proves both owners are independently needed. +Choose the first real blocker, evidence gap, or validation need. Do not preload methods because they might become useful later. ## Evidence Order -Prefer support signals in this order: +Prefer signals in this order: -1. Explicit user-selected skill or domain. +1. Explicit user direction. 2. File type, path family, runtime, framework, command, or schema surface. 3. Reproduced failure loop or validation output. -4. Cloud, provider, platform, or governance evidence in the prompt or files. -5. Existing nearby patterns in the same repository area. +4. Domain or platform evidence in the prompt or files. +5. Existing nearby patterns. -If no strong signal exists, do not guess. Stay in the quick lane with local -repository evidence, or escalate when the missing owner changes the risk. +If no strong signal exists, stay local or stop with reason. -## Support Buckets +## Method Buckets -| Signal | Support posture | Boundary | +| Signal | Method posture | Boundary | | --- | --- | --- | -| Missing intent, target path, input data, local context, or blocker prevents starting or continuing | Use `grill-me` only within the single-clarification limit in `references/clarification-gate.md`. | If the answer must settle ownership, rollout, governance, tradeoffs, validation strategy, or exceeds that clarification gate, escalate instead. | -| Bug, failing test, failing build, drift, or unexpected output | Use root-cause debugging support after reproducing the loop. | Do not patch from correlation alone. | -| Test-first request or executable behavior change | Use test-first support when a meaningful seam exists. | Do not force TDD onto prose, prompt, skill, inventory, or governance text without executable behavior. | -| Existing diff needs findings or merge readiness | Leave simple mode for review ownership. | Do not turn simple validation into defect-first review. | -| User asks to zoom out, understand unfamiliar code, or map modules and callers | Use `internal-high-level-review` as orientation support while staying descriptive. | Do not turn orientation into findings unless concrete systems risk is evidenced. | -| Architecture, workflow, cross-cutting impact, or blind spots dominate | Leave simple mode for systems review ownership. | Do not keep editing while ownership or rollout is unsettled. | -| Repository-owned skill, agent, prompt, or AI configuration work | Use the matching authoring owner when route, boundary, validation, or bundle structure changes. Inspect the owning bundle and nearest contract tests before calling it a pure copyedit. | Pure copyedits can remain simple after that bundle check. | -| Runtime, language, infrastructure, or platform file | Use the matching domain owner only after the path or task proves it. | Do not list or preload every possible operational skill. | -| Cloud or provider work | Use provider/domain support only when the prompt, files, commands, or validation surface identify it. | Do not infer unsupported status from an absent example. | -| Performance is the primary measured concern | Use performance support with baseline and before/after evidence. | Do not optimize from intuition alone. | -| Isolated workspace is required | Use worktree isolation support only when requested or needed to protect concurrent work. | Small answer, validation, and one-file edits should stay in the current workspace. | - -## Claim Gates - -Use this table when the task stays simple but the final answer would make a -strong status claim. This table is the single source of truth for claim-gate -ownership in simple mode. - -| Claim before final answer | Required owner | Evidence gate | -| --- | --- | --- | -| Original bug, failure, validator drift, or loop is fixed | `internal-debugging` | Re-run the original loop, or state the blocker. | -| Red-green-refactor passed or regression coverage exists | `internal-tdd` | Show the failing-then-passing seam, or state why it could not be run. | -| Performance improved | `internal-performance-optimization` | Compare baseline and after evidence from the same measurement class. | -| PR is ready, valid, mergeable, or complete | `internal-github-pr` | Check PR lifecycle evidence before the claim. | -| No code findings or code merge-readiness blockers exist | `internal-code-review` | Defect-first review evidence, or escalate to review mode. | -| No systems findings or systems merge-readiness blockers exist | `internal-high-level-review` | Systems review evidence, or escalate to review mode. | -| Any completion, readiness, no-findings, fixed, covered, or improved claim | `superpowers-verification-before-completion` | Fresh validation evidence, not intent or stale output. | - -If a required owner makes the work review-owned, staged, retained-plan-owned, or -critical-owned, stop simple mode and escalate instead of making the claim. - -Treat `validator passes` as a passing claim. Re-run the validator and read -fresh output before saying it passed. - -If the touched work includes auth, config, secrets, tenant data, or other -sensitive values, add a validation note confirming that nothing sensitive was -hardcoded, or state the exact gap. - -## Anti-Catalog Rule - -This reference is not a live inventory of repository skills. If a domain is not -named here, inspect repository evidence and available skills before deciding the -owner. The claim gates above are narrow status-claim protections, not a support -catalog. The simple gateway should stay high level while operational skills -activate only when called or proven by context. - -## Advisory Helper - -Run `scripts/resolve_simple_task.py gate` when the task facts are already -normalized and the bundle only needs a deterministic gate outcome plus a lean -Readiness Brief. - -Run `scripts/resolve_simple_task.py claim` before `fixed`, `covered`, -`validator passes`, `ready`, `no-findings`, or similar status claims when the -claim-gate owner is the noisy part. - -Run `scripts/suggest_support_skills.py` only when paths or symptoms are known -and support selection is noisy. Treat its output as hints. The agent still must -inspect local files and relevant domain skills before editing or claiming -policy. +| Missing intent, target path, input data, local context, or one blocker prevents starting | Ask one compact clarification block. | Stop if the answer would change scope, validation, cost, or risk. | +| Bug, failing test, failing build, drift, or unexpected output | Reproduce first, then debug by falsifiable hypothesis. | Do not patch from correlation alone. | +| Executable behavior change | Use the generic test-first loop when a meaningful seam exists. | Do not force it onto pure prose or governance wording with no executable seam. | +| Existing diff needs findings or merge-readiness | Stop with reason because the work is no longer simple execution. | Do not turn simple validation into review. | +| Orientation or unfamiliar code mapping | Stay descriptive and bounded. | Do not turn orientation into findings without concrete evidence. | +| Architecture, workflow, cross-cutting impact, or blind spots dominate | Stop with reason. | Do not keep editing while the boundary is unsettled. | +| Performance is the measured concern | Compare baseline and after evidence from the same measurement class. | Do not optimize from intuition alone. | +| Strong completion or passing claim | Run the final evidence gate with fresh validation evidence. | Do not rely on stale output or intent. | + +## Claim Discipline + +Use these evidence gates before strong claims: + +| Claim | Evidence gate | +| --- | --- | +| `fixed` | Re-run the original failing loop or state the blocker. | +| `covered` | Show the failing-then-passing seam, or state why it could not be run. | +| `performance-improved` | Compare baseline and after evidence from the same measurement class. | +| `validator-passes` | Re-run the validator and read fresh output before claiming success. | +| `completion`, `readiness`, `no-gap` | Confirm all in-scope work is closed and run the final evidence gate. | + +If the evidence gate would require broader staged work, stop with reason instead of over-claiming. + +## Advisory Helpers + +Run `scripts/resolve_simple_task.py gate` when the task facts are already normalized and only the gate outcome plus Readiness Brief is noisy. + +Run `scripts/resolve_simple_task.py claim` before strong status claims when the evidence requirements are the noisy part. + +Run `scripts/suggest_support_skills.py` only when path or symptom signals exist and the next method still needs a deterministic hint. diff --git a/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py b/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py index 1c97770f..24a2ac13 100644 --- a/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py +++ b/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py @@ -1,12 +1,5 @@ #!/usr/bin/env python3 -"""Deterministic gate and claim helper for internal-gateway-simple-task. - -The helper is intentionally structured. It expects normalized facts that were -already recovered from the user prompt, local files, or validation output. It -does not replace local inspection, but it reduces repeated reasoning when the -simple-task bundle only needs a deterministic gate, readiness brief, or claim -gate answer. -""" +"""Deterministic gate and claim helper for internal-gateway-simple-task.""" from __future__ import annotations @@ -17,7 +10,7 @@ DEPTH_KEYWORDS = ("full", "idea", "complete") TRIVIAL_KINDS = ("local-answer", "tiny-edit", "focused-read", "validator-run") -LANES = ("answer", "edit", "diagnose", "validate", "plan", "unspecified") +LANES = ("answer", "edit", "diagnose", "validate", "unspecified") MATERIAL_RISKS = ( "contract", "routing", @@ -34,107 +27,94 @@ "completion", "covered", "fixed", - "no-code-findings", - "no-systems-findings", + "no-gap", "performance-improved", - "pr-ready", "readiness", "validator-passes", ) -DIRECT_COMPLETION_REQUIREMENT = { - "owner": "internal-gateway-simple-task", - "evidence_gate": ( - "Run Direct Completion Control over every in-scope source item and " - "mandatory applicable requirement before the claim." - ), -} - CLAIM_REQUIREMENTS = { "fixed": [ { - "owner": "internal-debugging", - "evidence_gate": "Re-run the original loop, or state the blocker.", + "method": "reproduce-loop", + "evidence": "Re-run the original loop, or state the exact blocker.", + }, + { + "method": "scope-check", + "evidence": "Confirm every in-scope item is closed or explicitly deferred.", }, - DIRECT_COMPLETION_REQUIREMENT, { - "owner": "superpowers-verification-before-completion", - "evidence_gate": "Fresh validation evidence, not intent or stale output.", + "method": "superpowers-verification-before-completion", + "evidence": "Use fresh validation evidence, not intent or stale output.", }, ], "covered": [ { - "owner": "internal-tdd", - "evidence_gate": "Show the failing-then-passing seam, or state why it could not be run.", + "method": "test-first", + "evidence": "Show the failing-then-passing seam, or state why it could not be run.", }, - DIRECT_COMPLETION_REQUIREMENT, { - "owner": "superpowers-verification-before-completion", - "evidence_gate": "Fresh validation evidence, not intent or stale output.", + "method": "scope-check", + "evidence": "Confirm the changed behavior and in-scope files are fully covered.", + }, + { + "method": "superpowers-verification-before-completion", + "evidence": "Use fresh validation evidence, not intent or stale output.", }, ], "performance-improved": [ { - "owner": "internal-performance-optimization", - "evidence_gate": "Compare baseline and after evidence from the same measurement class.", + "method": "measurement-compare", + "evidence": "Compare baseline and after evidence from the same measurement class.", }, - DIRECT_COMPLETION_REQUIREMENT, { - "owner": "superpowers-verification-before-completion", - "evidence_gate": "Fresh validation evidence, not intent or stale output.", + "method": "scope-check", + "evidence": "Confirm no unverified tradeoff remains inside the touched scope.", + }, + { + "method": "superpowers-verification-before-completion", + "evidence": "Use fresh validation evidence, not intent or stale output.", }, ], - "pr-ready": [ + "completion": [ { - "owner": "internal-github-pr", - "evidence_gate": "Check PR lifecycle evidence before the claim.", + "method": "scope-check", + "evidence": "Confirm every in-scope source item is closed with observable evidence.", }, - DIRECT_COMPLETION_REQUIREMENT, { - "owner": "superpowers-verification-before-completion", - "evidence_gate": "Fresh validation evidence, not intent or stale output.", + "method": "superpowers-verification-before-completion", + "evidence": "Use fresh validation evidence before the completion claim.", }, ], - "no-code-findings": [ + "readiness": [ { - "owner": "internal-code-review", - "evidence_gate": "Defect-first review evidence, or escalate to review mode.", + "method": "scope-check", + "evidence": "Confirm all required work is closed and no stop condition remains open.", }, { - "owner": "superpowers-verification-before-completion", - "evidence_gate": "Fresh validation evidence, not intent or stale output.", + "method": "superpowers-verification-before-completion", + "evidence": "Use fresh validation evidence before the readiness claim.", }, ], - "no-systems-findings": [ + "validator-passes": [ { - "owner": "internal-high-level-review", - "evidence_gate": "Systems review evidence, or escalate to review mode.", + "method": "rerun-validator", + "evidence": "Re-run the validator and read fresh output before the claim.", }, { - "owner": "superpowers-verification-before-completion", - "evidence_gate": "Fresh validation evidence, not intent or stale output.", + "method": "superpowers-verification-before-completion", + "evidence": "Use fresh validation evidence before the passing claim.", }, ], - "completion": [ - DIRECT_COMPLETION_REQUIREMENT, + "no-gap": [ { - "owner": "superpowers-verification-before-completion", - "evidence_gate": "Fresh validation evidence, not intent or stale output.", - } - ], - "readiness": [ - DIRECT_COMPLETION_REQUIREMENT, - { - "owner": "superpowers-verification-before-completion", - "evidence_gate": "Fresh validation evidence, not intent or stale output.", - } - ], - "validator-passes": [ - DIRECT_COMPLETION_REQUIREMENT, + "method": "scope-check", + "evidence": "Confirm no in-scope item, validation gap, or stop condition remains open.", + }, { - "owner": "superpowers-verification-before-completion", - "evidence_gate": "Re-run the validator and read fresh output before the claim.", - } + "method": "superpowers-verification-before-completion", + "evidence": "Use fresh validation evidence before the no-gap claim.", + }, ], } @@ -145,191 +125,127 @@ def parse_args() -> argparse.Namespace: ) subparsers = parser.add_subparsers(dest="command", required=True) - gate_parser = subparsers.add_parser( - "gate", help="Classify the simple-task gate from normalized facts." - ) + gate_parser = subparsers.add_parser("gate", help="Classify the gate from normalized facts.") gate_parser.add_argument("--task", default="unspecified task") - gate_parser.add_argument( - "--lane", choices=LANES, default="unspecified", help="Active simple lane." - ) - gate_parser.add_argument( - "--trivial-kind", - choices=TRIVIAL_KINDS, - help="Normalized local task kind for trivial-skip checks.", - ) - gate_parser.add_argument( - "--prompt", - default="", - help="Optional prompt text used only for deterministic depth-keyword detection.", - ) + gate_parser.add_argument("--lane", choices=LANES, default="unspecified") + gate_parser.add_argument("--trivial-kind", choices=TRIVIAL_KINDS) + gate_parser.add_argument("--prompt", default="") gate_parser.add_argument( "--depth-keyword", action="append", choices=DEPTH_KEYWORDS, default=[], - help="Known depth keyword. Repeatable.", ) gate_parser.add_argument( "--risk", action="append", choices=MATERIAL_RISKS, default=[], - help="Known material risk. Repeatable.", ) gate_parser.add_argument("--needs-plan", action="store_true") gate_parser.add_argument("--needs-review", action="store_true") gate_parser.add_argument("--needs-critical", action="store_true") - gate_parser.add_argument("--needs-retained-plan", action="store_true") - gate_parser.add_argument( - "--plan-mode", - choices=("explicit", "implicit"), - help="Plan mode trigger type. Explicit is mandatory from the user; implicit is a cost-signal proposal.", - ) gate_parser.add_argument("--owner-ambiguous", action="store_true") gate_parser.add_argument("--clarification-overflow", action="store_true") gate_parser.add_argument("--validation-obvious", action="store_true") - gate_parser.add_argument( - "--validation-path", default="", help="Focused validation command or check." - ) - gate_parser.add_argument( - "--validation-gap", default="", help="Exact validation gap when no check exists yet." - ) - gate_parser.add_argument( - "--format", choices=("text", "json"), default="text", help="Output format." - ) + gate_parser.add_argument("--validation-path", default="") + gate_parser.add_argument("--validation-gap", default="") + gate_parser.add_argument("--format", choices=("text", "json"), default="text") claim_parser = subparsers.add_parser( - "claim", help="Resolve required owners and evidence gates for status claims." + "claim", help="Resolve evidence gates for strong status claims." ) claim_parser.add_argument( "--claim", action="append", choices=CLAIMS, required=True, - help="Status claim to evaluate. Repeatable.", - ) - claim_parser.add_argument( - "--format", choices=("text", "json"), default="text", help="Output format." ) - + claim_parser.add_argument("--format", choices=("text", "json"), default="text") return parser.parse_args() def detect_depth_keywords(prompt: str, explicit_keywords: list[str]) -> list[str]: found = set(explicit_keywords) lowered_prompt = prompt.lower() - for keyword in DEPTH_KEYWORDS: if re.search(rf"\b{re.escape(keyword)}\b", lowered_prompt): found.add(keyword) - return sorted(found) def infer_lane(lane: str, trivial_kind: str | None) -> str: if lane != "unspecified": return lane - if trivial_kind == "local-answer": - return "answer" - if trivial_kind == "focused-read": - return "answer" - if trivial_kind == "tiny-edit": - return "edit" - if trivial_kind == "validator-run": - return "validate" - return "unspecified" - - -def determine_next_owner( + mapping = { + "local-answer": "answer", + "focused-read": "answer", + "tiny-edit": "edit", + "validator-run": "validate", + } + return mapping.get(trivial_kind or "", "unspecified") + + +def build_gate_decision( *, + task: str, + lane: str, + trivial_kind: str | None, + prompt: str, + depth_keywords: list[str], + risks: list[str], needs_plan: bool, needs_review: bool, needs_critical: bool, - needs_retained_plan: bool, - plan_mode: str | None, owner_ambiguous: bool, clarification_overflow: bool, -) -> str: - if needs_review: - return "internal-gateway-review" - if needs_critical: - return "internal-gateway-critical-master" - if plan_mode: - return "internal-gateway-simple-task" - if ( - needs_plan - or needs_retained_plan - or owner_ambiguous - or clarification_overflow - ): - return "internal-gateway-idea-brainstorming" - return "internal-gateway-simple-task" - - -def build_gate_decision( - *, - task: str, - lane: str = "unspecified", - trivial_kind: str | None = None, - prompt: str = "", - depth_keywords: list[str] | None = None, - risks: list[str] | None = None, - needs_plan: bool = False, - needs_review: bool = False, - needs_critical: bool = False, - needs_retained_plan: bool = False, - plan_mode: str | None = None, - owner_ambiguous: bool = False, - clarification_overflow: bool = False, - validation_obvious: bool = False, - validation_path: str = "", - validation_gap: str = "", + validation_obvious: bool, + validation_path: str, + validation_gap: str, ) -> dict[str, object]: - resolved_depth_keywords = detect_depth_keywords(prompt, depth_keywords or []) - resolved_risks = sorted(set(risks or [])) + resolved_depth_keywords = detect_depth_keywords(prompt, depth_keywords) + resolved_risks = sorted(set(risks)) resolved_lane = infer_lane(lane, trivial_kind) - next_owner = determine_next_owner( - needs_plan=needs_plan, - needs_review=needs_review, - needs_critical=needs_critical, - needs_retained_plan=needs_retained_plan, - plan_mode=plan_mode, - owner_ambiguous=owner_ambiguous, - clarification_overflow=clarification_overflow, + stop_reasons: list[str] = [] + stop_for_material_boundary = bool(validation_gap) and any( + risk in {"architecture", "cross-file", "governance", "rollout"} + for risk in resolved_risks ) - reasons: list[str] = [] + if needs_plan: + stop_reasons.append("plan-recommended") if needs_review: - reasons.append("needs-review") + stop_reasons.append("review-shaped") if needs_critical: - reasons.append("needs-critical") - if needs_plan: - reasons.append("needs-plan") - if needs_retained_plan: - reasons.append("needs-retained-plan") - if plan_mode: - reasons.append(f"plan-mode:{plan_mode}") + stop_reasons.append("critical-challenge-needed") if owner_ambiguous: - reasons.append("owner-ambiguous") + stop_reasons.append("owner-ambiguous") if clarification_overflow: - reasons.append("clarification-overflow") + stop_reasons.append("clarification-overflow") for keyword in resolved_depth_keywords: - reasons.append(f"depth-keyword:{keyword}") + stop_reasons.append(f"depth-keyword:{keyword}") for risk in resolved_risks: - reasons.append(f"material-risk:{risk}") + stop_reasons.append(f"material-risk:{risk}") + if stop_for_material_boundary: + stop_reasons.append("material-boundary-break") - if next_owner != "internal-gateway-simple-task": - gate_outcome = "escalate" - elif plan_mode: - gate_outcome = "plan-mode" + if ( + needs_plan + or needs_review + or owner_ambiguous + or clarification_overflow + or stop_for_material_boundary + ): + gate_outcome = "stop-with-reason" elif ( trivial_kind in TRIVIAL_KINDS and not resolved_depth_keywords and not resolved_risks + and not needs_critical and (validation_obvious or bool(validation_path) or bool(validation_gap)) ): gate_outcome = "trivial-skip" - reasons.append(f"trivial-kind:{trivial_kind}") + stop_reasons.append(f"trivial-kind:{trivial_kind}") else: gate_outcome = "full-gate" @@ -342,49 +258,42 @@ def build_gate_decision( else: focused_validation_path = "Validation path not yet identified." - if resolved_risks: - primary_assumption_or_risk = "Material risk: " + ", ".join(resolved_risks) + if gate_outcome == "trivial-skip": + main_risk = "Task stays local and validation is already bounded." + elif resolved_risks: + main_risk = "Material risk: " + ", ".join(resolved_risks) elif resolved_depth_keywords: - primary_assumption_or_risk = "Depth keyword present: " + ", ".join( - resolved_depth_keywords - ) - elif gate_outcome == "trivial-skip": - primary_assumption_or_risk = ( - "Task stays local and validation is already obvious or explicitly bounded." - ) + main_risk = "Depth keyword present: " + ", ".join(resolved_depth_keywords) + elif needs_critical: + main_risk = "Non-trivial work needs critical challenge before action." else: - primary_assumption_or_risk = ( - "Task still fits one quick lane but needs the full gate before action." - ) + main_risk = "Task still fits one bounded run but needs the full gate before action." - if plan_mode == "implicit": - approval_checkpoint = ( - "explicit user approval before writing the retained plan " - "(implicit cost-signal proposal)" - ) - elif plan_mode == "explicit": - approval_checkpoint = ( - "plan mode requested by user; confirm profile and proceed to retained-plan authoring" - ) + if gate_outcome == "stop-with-reason": + approval = "do not execute; wait for a user decision after reporting the stop reason" else: - approval_checkpoint = "explicit user approval before operational work" + approval = "explicit user approval before non-trivial operational work" readiness_brief = { "task": task, - "lane_owner": next_owner, - "primary_assumption_or_risk": primary_assumption_or_risk, - "focused_validation_path": focused_validation_path, - "gate_outcome": gate_outcome, - "approval_checkpoint": approval_checkpoint, + "goal": "Complete the current bounded task in one run when safe.", + "scope": f"Single-lane {resolved_lane} work." if resolved_lane != "unspecified" else "Single-lane work.", + "anti_scope": "No staged workflow expansion, no hidden delegation, no speculative side work.", + "files_expected": "Name only the files proven by local evidence.", + "approach": "Use the smallest coherent move that preserves validation coverage.", + "executable_behavior": "Use the generic test-first loop when behavior changes and a useful seam exists.", + "validation_path": focused_validation_path, + "main_risk": main_risk, + "stop_conditions": "Stop for complexity, cost, ambiguity, safety, approval, or validation gaps.", + "approval": approval, } return { "gate_outcome": gate_outcome, + "next_action": "stop" if gate_outcome == "stop-with-reason" else "execute", "lane": resolved_lane, - "next_owner": next_owner, - "depth_keywords": resolved_depth_keywords, - "reason_codes": reasons, - "needs_explicit_approval": next_owner == "internal-gateway-simple-task", + "reason_codes": stop_reasons, + "needs_explicit_approval": gate_outcome != "trivial-skip", "readiness_brief": readiness_brief, } @@ -392,53 +301,47 @@ def build_gate_decision( def resolve_claim_requirements(claims: list[str]) -> list[dict[str, str]]: ordered: list[dict[str, str]] = [] seen: set[tuple[str, str]] = set() - for claim in claims: for requirement in CLAIM_REQUIREMENTS[claim]: - key = (requirement["owner"], requirement["evidence_gate"]) + key = (requirement["method"], requirement["evidence"]) if key in seen: continue seen.add(key) ordered.append(requirement) - return ordered def render_gate_text(decision: dict[str, object]) -> None: - readiness_brief = decision["readiness_brief"] + brief = decision["readiness_brief"] print(f"Gate outcome: {decision['gate_outcome']}") + print(f"Next action: {decision['next_action']}") print(f"Lane: {decision['lane']}") - print(f"Next owner: {decision['next_owner']}") - if decision["depth_keywords"]: - print("Depth keywords: " + ", ".join(decision["depth_keywords"])) if decision["reason_codes"]: print("Reason codes:") for reason in decision["reason_codes"]: print(f"- {reason}") print("Readiness Brief:") - print(f"- task: {readiness_brief['task']}") - print(f"- lane-owner: {readiness_brief['lane_owner']}") - print( - "- primary assumption or risk: " - f"{readiness_brief['primary_assumption_or_risk']}" - ) - print( - "- focused validation path: " - f"{readiness_brief['focused_validation_path']}" - ) - print(f"- gate outcome: {readiness_brief['gate_outcome']}") - print(f"- approval checkpoint: {readiness_brief['approval_checkpoint']}") + print(f"- Task: {brief['task']}") + print(f"- Goal: {brief['goal']}") + print(f"- Scope: {brief['scope']}") + print(f"- Anti-scope: {brief['anti_scope']}") + print(f"- Files expected: {brief['files_expected']}") + print(f"- Approach: {brief['approach']}") + print(f"- Executable behavior: {brief['executable_behavior']}") + print(f"- Validation path: {brief['validation_path']}") + print(f"- Main risk: {brief['main_risk']}") + print(f"- Stop conditions: {brief['stop_conditions']}") + print(f"- Approval: {brief['approval']}") def render_claim_text(claims: list[str], requirements: list[dict[str, str]]) -> None: print("Claims: " + ", ".join(claims)) for requirement in requirements: - print(f"- {requirement['owner']}: {requirement['evidence_gate']}") + print(f"- {requirement['method']}: {requirement['evidence']}") def main() -> int: args = parse_args() - if args.command == "gate": decision = build_gate_decision( task=args.task, @@ -450,8 +353,6 @@ def main() -> int: needs_plan=args.needs_plan, needs_review=args.needs_review, needs_critical=args.needs_critical, - needs_retained_plan=args.needs_retained_plan, - plan_mode=args.plan_mode, owner_ambiguous=args.owner_ambiguous, clarification_overflow=args.clarification_overflow, validation_obvious=args.validation_obvious, diff --git a/.github/skills/internal-gateway-simple-task/scripts/suggest_support_skills.py b/.github/skills/internal-gateway-simple-task/scripts/suggest_support_skills.py index 6abd923f..b2ece7f2 100644 --- a/.github/skills/internal-gateway-simple-task/scripts/suggest_support_skills.py +++ b/.github/skills/internal-gateway-simple-task/scripts/suggest_support_skills.py @@ -1,13 +1,5 @@ #!/usr/bin/env python3 -"""Suggest path and symptom-based support hints for internal-gateway-simple-task. - -The script is advisory. It maps known paths and symptoms to the narrowest -currently proved repository owner. It should not expand one path into a -base-plus-depth stack or a referenced-skill chain when one owner already fits. -Absence from this helper is not evidence that a provider, runtime, or domain -is unsupported. Final selection still belongs to repository evidence, -path-to-skill routing, and explicit user-selected skills. -""" +"""Suggest generic method hints for internal-gateway-simple-task.""" from __future__ import annotations @@ -16,213 +8,97 @@ from pathlib import Path -SYMPTOM_SKILLS = { - "blocked": ("grill-me", "A blocker needs minimum clarification before continuing."), - "bug": ("internal-debugging", "Bug or unexpected behavior needs reproduction."), - "test-failure": ("internal-debugging", "A failing test needs root-cause diagnosis."), - "build-failure": ("internal-debugging", "A failing build needs root-cause diagnosis."), - "validator-drift": ("internal-debugging", "Validator drift needs a reproducible loop."), - "unexpected": ("internal-debugging", "Unexpected output needs diagnosis before patching."), - "missing-context": ("grill-me", "Missing context prevents starting the simple lane."), - "tdd": ("internal-tdd", "Executable behavior should be delivered test-first."), - "performance": ("internal-performance-optimization", "Performance is the primary measured concern."), - "pr-readiness": ("internal-github-pr", "PR readiness, validity, mergeability, or completion needs PR lifecycle evidence."), - "code-review": ("internal-code-review", "Line-level code review evidence is requested."), - "systems-review": ("internal-high-level-review", "Cross-boundary or architecture review evidence is requested."), - "completion-claim": ("superpowers-verification-before-completion", "Completion or readiness claim needs fresh validation evidence."), - "no-findings": ("internal-code-review", "No-findings claim needs defect-first review evidence."), - "worktree": ("superpowers-using-git-worktrees", "The task needs isolated workspace setup."), +SYMPTOM_METHODS = { + "blocked": ("clarify", "A blocker needs one focused clarification before continuing."), + "bug": ("diagnose", "Unexpected behavior needs reproduction before patching."), + "test-failure": ("diagnose", "A failing test needs root-cause diagnosis."), + "build-failure": ("diagnose", "A failing build needs root-cause diagnosis."), + "validator-drift": ("diagnose", "Validator drift needs a reproducible loop."), + "unexpected": ("diagnose", "Unexpected output needs diagnosis before patching."), + "missing-context": ("clarify", "Missing context prevents starting the current lane."), + "tdd": ("test-first", "Executable behavior should be delivered through the smallest useful seam."), + "performance": ("measure-performance", "Performance is the primary measured concern."), + "pr-readiness": ("lifecycle-check", "Readiness claims need lifecycle evidence before the final answer."), + "code-review": ("review-shape-stop", "The work is becoming findings-first rather than execution-first."), + "systems-review": ("review-shape-stop", "Cross-boundary analysis is dominating the task."), + "completion-claim": ("verify-claim", "Completion or readiness claims need fresh validation evidence."), + "no-findings": ("review-shape-stop", "No-findings claims need findings-first evidence."), + "worktree": ("isolate-workspace-needed", "The task may need isolated workspace protection."), } def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( - description="Suggest support skills for a concrete simple task." + description="Suggest generic support methods for a concrete simple task." ) parser.add_argument("paths", nargs="*", help="Target files or directories.") parser.add_argument( "--symptom", action="append", - choices=sorted(SYMPTOM_SKILLS), + choices=sorted(SYMPTOM_METHODS), default=[], help="Known task symptom. Repeatable.", ) - parser.add_argument( - "--format", - choices=("text", "json"), - default="text", - help="Output format.", - ) + parser.add_argument("--format", choices=("text", "json"), default="text") return parser.parse_args() -def add(suggestions: dict[str, set[str]], skill: str, reason: str) -> None: - suggestions.setdefault(skill, set()).add(reason) +def add(suggestions: dict[str, set[str]], method: str, reason: str) -> None: + suggestions.setdefault(method, set()).add(reason) def normalize_path_text(path_text: str) -> str: path = Path(path_text).expanduser() - if path.is_absolute(): repo_root = Path.cwd().resolve() - try: return path.resolve().relative_to(repo_root).as_posix() except ValueError: return path.as_posix() - return path.as_posix() -def is_script_path(lowered_path: str) -> bool: - return lowered_path.startswith((".github/scripts/", "scripts/")) or "/scripts/" in lowered_path - - -def is_python_project_path(lowered_path: str) -> bool: - return lowered_path.startswith(("src/", "app/", "services/")) or any( - marker in lowered_path for marker in ("/src/", "/app/", "/services/") - ) - - -def is_node_project_path(lowered_path: str) -> bool: - return lowered_path.startswith(("src/", "app/", "services/", "api/")) or any( - marker in lowered_path - for marker in ("/src/", "/app/", "/services/", "/api/") - ) - - -def is_java_project_path(lowered_path: str) -> bool: - return lowered_path.startswith(("src/main/java/", "src/test/java/")) or any( - marker in lowered_path for marker in ("/src/main/java/", "/src/test/java/") - ) - - def suggest_for_path(path_text: str, suggestions: dict[str, set[str]]) -> None: normalized = normalize_path_text(path_text) lowered = normalized.lower() name = Path(normalized).name.lower() suffix = Path(lowered).suffix - if normalized.startswith(".github/skills/internal-"): - add(suggestions, "internal-skill-creator", "Repository-owned skill path.") - return - if normalized.startswith(".github/agents/internal-"): - add(suggestions, "internal-agent-creator", "Repository-owned agent path.") + if normalized.startswith(".github/skills/internal-") or normalized.startswith(".github/agents/internal-"): + add(suggestions, "bundle-contract-check", "Repository-owned bundle paths need contract and local-validator attention.") return if name == "codeowners": - add(suggestions, "internal-github-governance", "CODEOWNERS governance path.") + add(suggestions, "governance-check", "Ownership files need governance-aware validation.") return if name in {"makefile", "gnumakefile"} or suffix == ".mk": - add(suggestions, "internal-makefile", "Makefile or make include path.") - return - if ( - name in {"azure-pipelines.yml", "azure-pipelines.yaml"} - or name.startswith("azure-pipelines") - or name.endswith(".pipeline.yml") - or name.endswith(".pipeline.yaml") - ): - add(suggestions, "internal-azure-devops", "Azure DevOps pipeline path.") - return - if normalized in {".github/dependabot.yml", ".github/dependabot.yaml"}: - add(suggestions, "awesome-copilot-dependabot", "Dependabot configuration path.") - return - if name in { - "package-lock.json", - "npm-shrinkwrap.json", - "pnpm-lock.yaml", - "yarn.lock", - "bun.lock", - "bun.lockb", - }: - add(suggestions, "internal-nodejs", "Node.js package-manager lockfile path.") + add(suggestions, "command-surface-check", "Build orchestration files need focused command validation.") return - if name.startswith("dockerfile") or "docker-compose" in name or "compose." in name: - add(suggestions, "internal-docker", "Docker or Compose path.") + if "/workflows/" in lowered or "/actions/" in lowered: + add(suggestions, "automation-check", "Automation files need focused workflow validation.") return - if name == "chart.yaml": - add(suggestions, "internal-kubernetes", "Helm chart metadata path.") + if suffix in {".py", ".sh", ".go", ".js", ".cjs", ".mjs", ".ts", ".tsx", ".java"}: + add(suggestions, "runtime-check", "Executable source files need the closest runnable or syntax validation.") return - if "/workflows/" in lowered and suffix in {".yml", ".yaml"}: - add(suggestions, "internal-github-actions", "GitHub Actions workflow path.") - return - if "/actions/" in lowered and name in {"action.yml", "action.yaml"}: - add(suggestions, "internal-github-action-composite", "Composite action metadata path.") - return - if any(part in lowered for part in ("k8s/", "manifests/", "charts/")) and suffix in { - ".yml", - ".yaml", - }: - add(suggestions, "internal-kubernetes", "Kubernetes manifest path.") - return - if "lambda" in lowered and suffix in {".py", ".js", ".ts", ".tf"}: - add(suggestions, "internal-aws-lambda", "Lambda-related file path.") + if suffix in {".yml", ".yaml", ".json", ".tf"}: + add(suggestions, "config-check", "Configuration files need schema-aware or command-aware validation.") return - - if suffix == ".py": - if is_script_path(lowered): - add(suggestions, "internal-python-script", "Python operational script path.") - elif is_python_project_path(lowered): - add(suggestions, "internal-python-project", "Python package or application path.") - else: - add(suggestions, "internal-python", "Python file path.") - elif suffix == ".sh": - if is_script_path(lowered) or name == "run.sh": - add(suggestions, "internal-bash-script", "Bash script path.") - else: - add(suggestions, "internal-bash", "Shell or Bash file path.") - elif suffix == ".tf": - add(suggestions, "internal-terraform", "Terraform file path.") - elif suffix == ".go" or name in {"go.mod", "go.sum"}: - add(suggestions, "internal-go", "Go source or module path.") - elif suffix in {".js", ".cjs", ".mjs", ".ts", ".tsx"} or name in { - "package.json", - "tsconfig.json", - }: - if name in {"package.json", "tsconfig.json"}: - add(suggestions, "internal-nodejs", "JavaScript, Node.js, or TypeScript metadata path.") - elif is_node_project_path(lowered): - add(suggestions, "internal-nodejs-project", "Node.js or TypeScript project path.") - else: - add(suggestions, "internal-nodejs", "JavaScript, Node.js, or TypeScript path.") - elif suffix == ".java": - if is_java_project_path(lowered): - add(suggestions, "internal-java-project", "Java project path.") - else: - add(suggestions, "internal-java", "Java source path.") - elif name in {"pom.xml", "build.gradle", "build.gradle.kts"}: - add(suggestions, "internal-java", "Java build metadata path.") - elif suffix == ".md": - add(suggestions, "internal-markdown", "Markdown file path.") - elif suffix == ".json" and ( - lowered.startswith("config/") - or "/config/" in lowered - or any( - part in lowered for part in ("authorizations/", "organization/", "src/", "data/") - ) - ): - add(suggestions, "internal-json", "Repository-owned JSON registry or data path.") - - elif suffix in {".yml", ".yaml"}: - add(suggestions, "internal-yaml", "YAML file path.") + if suffix == ".md": + add(suggestions, "authoring-check", "Prose bundles need contract consistency and compact evidence.") def render_text(suggestions: dict[str, set[str]]) -> None: if not suggestions: - print( - "No path or symptom-based support hint. Inspect files, domain skills, " - "and explicit user-selected owners first." - ) + print("No deterministic method hint. Inspect local evidence and choose the smallest bounded next move.") return - - for skill in sorted(suggestions): - reasons = "; ".join(sorted(suggestions[skill])) - print(f"- {skill}: {reasons}") + for method in sorted(suggestions): + reasons = "; ".join(sorted(suggestions[method])) + print(f"- {method}: {reasons}") def render_json(suggestions: dict[str, set[str]]) -> None: payload = [ - {"skill": skill, "reasons": sorted(reasons)} - for skill, reasons in sorted(suggestions.items()) + {"method": method, "reasons": sorted(reasons)} + for method, reasons in sorted(suggestions.items()) ] print(json.dumps(payload, indent=2)) @@ -230,10 +106,9 @@ def render_json(suggestions: dict[str, set[str]]) -> None: def main() -> int: args = parse_args() suggestions: dict[str, set[str]] = {} - for symptom in args.symptom: - skill, reason = SYMPTOM_SKILLS[symptom] - add(suggestions, skill, reason) + method, reason = SYMPTOM_METHODS[symptom] + add(suggestions, method, reason) for path_text in args.paths: suggest_for_path(path_text, suggestions) From 5d03726f4858bff6460ccc808c661f4ba9c33105 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Mon, 6 Jul 2026 22:40:33 +0200 Subject: [PATCH 05/59] docs: enhance SKILL.md and output-contract.md with additional clarity on usage and synthesis requirements --- .github/skills/internal-gateway-critical-master/SKILL.md | 8 +++++++- .../references/output-contract.md | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/skills/internal-gateway-critical-master/SKILL.md b/.github/skills/internal-gateway-critical-master/SKILL.md index e683de18..213314e6 100644 --- a/.github/skills/internal-gateway-critical-master/SKILL.md +++ b/.github/skills/internal-gateway-critical-master/SKILL.md @@ -5,6 +5,10 @@ description: Use when a repository-owned plan, proposal, or decision needs a cri # Internal Gateway Critical Master +## Referenced skills + +- None. + Use this skill as the portable core for critical challenge work. The calling gateway decides when to invoke it; this skill challenges only. @@ -35,7 +39,8 @@ Run exactly three phases. Do not skip a phase and do not loop back unless new ev - Select **2-3 lenses** from the table below based on the highest-risk gaps in the summary. - The **third lens must be lateral**: `analogy` or `reverse assumption`. - Apply one optional pre-mortem pass if failure modes are material and not covered by the selected lenses. -- Ask probing questions only when the answer changes the critique. +- Lead with the strongest supported objection first. Stop at one finding when that objection controls the decision; do not pad findings. +- Ask exactly one concise root question only when the answer would materially change the critique. - Output: 1-3 raw findings, each with a claim class and a note on evidence quality. | Lens | Question | Use when | @@ -63,6 +68,7 @@ For a pre-mortem, state one concrete failure, list the 2-3 most likely root caus ### Phase 3: Synthesize - Run the Final Consistency Gate: name the strongest supported objection, downgrade weak claims to hypotheses, and surface unresolved uncertainty. +- When the user has already defended the proposal, include the strongest defense and the remaining vulnerability inside the synthesis. - Format the result using the contract in `references/output-contract.md`. - Recommend exactly one outcome from `## Outcome meanings`. diff --git a/.github/skills/internal-gateway-critical-master/references/output-contract.md b/.github/skills/internal-gateway-critical-master/references/output-contract.md index 3350a0d6..e081d825 100644 --- a/.github/skills/internal-gateway-critical-master/references/output-contract.md +++ b/.github/skills/internal-gateway-critical-master/references/output-contract.md @@ -13,7 +13,7 @@ Use this reference to produce a compact, consistent critical challenge deliverab | `finding.evidence` | Repository evidence, inference, or named uncertainty. | 30 words | | `finding.mitigation` | Condition or action required before execution resumes. | 30 words | | `finding.reframe` | Optional lateral reframe. | 25 words | -| `synthesis` | Result of the Final Consistency Gate. | 100 words | +| `synthesis` | Result of the Final Consistency Gate, including any strongest defense and remaining vulnerability when user defenses exist. | 100 words | | `outcome` | Exactly one value from `## Outcome meanings` in `SKILL.md`. | 1 value | ## Budget From eda7e5bb4e4b9b6b8a38797800bbe23da53f7a9d Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Mon, 6 Jul 2026 22:57:53 +0200 Subject: [PATCH 06/59] feat: add optional root question field to findings and update related validation logic --- .../internal-gateway-critical-master/SKILL.md | 5 +-- .../fixtures/critical_output_valid.md | 1 + .../references/output-contract.md | 5 ++- .../scripts/critical_master.py | 7 +++- .../scripts/validate_critical_output.py | 16 +++++++++ tests/test_critical_master_helpers.py | 13 +++++++ tests/test_critical_master_validator.py | 35 +++++++++++++++++++ 7 files changed, 78 insertions(+), 4 deletions(-) diff --git a/.github/skills/internal-gateway-critical-master/SKILL.md b/.github/skills/internal-gateway-critical-master/SKILL.md index 213314e6..483e464b 100644 --- a/.github/skills/internal-gateway-critical-master/SKILL.md +++ b/.github/skills/internal-gateway-critical-master/SKILL.md @@ -40,7 +40,8 @@ Run exactly three phases. Do not skip a phase and do not loop back unless new ev - The **third lens must be lateral**: `analogy` or `reverse assumption`. - Apply one optional pre-mortem pass if failure modes are material and not covered by the selected lenses. - Lead with the strongest supported objection first. Stop at one finding when that objection controls the decision; do not pad findings. -- Ask exactly one concise root question only when the answer would materially change the critique. +- Ask exactly one concise root question only when the answer would materially change the critique, and put it in `finding.question`. +- Treat mitigations as conditions to continue, not as implementation designs that rescue the proposal. - Output: 1-3 raw findings, each with a claim class and a note on evidence quality. | Lens | Question | Use when | @@ -68,7 +69,7 @@ For a pre-mortem, state one concrete failure, list the 2-3 most likely root caus ### Phase 3: Synthesize - Run the Final Consistency Gate: name the strongest supported objection, downgrade weak claims to hypotheses, and surface unresolved uncertainty. -- When the user has already defended the proposal, include the strongest defense and the remaining vulnerability inside the synthesis. +- When the user has already defended the proposal, classify the defense as `resolves`, `narrows`, `accepts-risk`, or `unanswered`, then name the strongest defense and the remaining vulnerability inside the synthesis. - Format the result using the contract in `references/output-contract.md`. - Recommend exactly one outcome from `## Outcome meanings`. diff --git a/.github/skills/internal-gateway-critical-master/fixtures/critical_output_valid.md b/.github/skills/internal-gateway-critical-master/fixtures/critical_output_valid.md index 4621746e..ddd39a0b 100644 --- a/.github/skills/internal-gateway-critical-master/fixtures/critical_output_valid.md +++ b/.github/skills/internal-gateway-critical-master/fixtures/critical_output_valid.md @@ -10,6 +10,7 @@ We are challenging a proposal to move validation logic from CI into a pre-commit - **Evidence:** `inference` - no replacement logging is described. - **Mitigation:** Add a signed attestation step before the hook is enabled. - **Reframe:** Treat local validation as an early filter, not a replacement for CI. +- **Question:** Which central audit record replaces the CI validation log? ## Synthesis diff --git a/.github/skills/internal-gateway-critical-master/references/output-contract.md b/.github/skills/internal-gateway-critical-master/references/output-contract.md index e081d825..313ad135 100644 --- a/.github/skills/internal-gateway-critical-master/references/output-contract.md +++ b/.github/skills/internal-gateway-critical-master/references/output-contract.md @@ -13,7 +13,8 @@ Use this reference to produce a compact, consistent critical challenge deliverab | `finding.evidence` | Repository evidence, inference, or named uncertainty. | 30 words | | `finding.mitigation` | Condition or action required before execution resumes. | 30 words | | `finding.reframe` | Optional lateral reframe. | 25 words | -| `synthesis` | Result of the Final Consistency Gate, including any strongest defense and remaining vulnerability when user defenses exist. | 100 words | +| `finding.question` | Optional single root question when the answer would materially change the critique. | 25 words | +| `synthesis` | Result of the Final Consistency Gate, including defense classification, strongest defense, and remaining vulnerability when user defenses exist. | 100 words | | `outcome` | Exactly one value from `## Outcome meanings` in `SKILL.md`. | 1 value | ## Budget @@ -37,6 +38,7 @@ Use this reference to produce a compact, consistent critical challenge deliverab - **Evidence:** - **Mitigation:** - **Reframe:** +- **Question:** ## Synthesis @@ -62,6 +64,7 @@ We are challenging a proposal to move validation logic from CI into a pre-commit - **Evidence:** `inference` — no replacement logging is described. - **Mitigation:** Add a signed attestation step before the hook is enabled. - **Reframe:** Treat local validation as an early filter, not a replacement for CI. (Analogy: like a spell-checker that does not replace a full code review.) +- **Question:** Which central audit record replaces the CI validation log? ## Synthesis diff --git a/.github/skills/internal-gateway-critical-master/scripts/critical_master.py b/.github/skills/internal-gateway-critical-master/scripts/critical_master.py index 30bb1cb2..ea1ce86b 100644 --- a/.github/skills/internal-gateway-critical-master/scripts/critical_master.py +++ b/.github/skills/internal-gateway-critical-master/scripts/critical_master.py @@ -39,13 +39,14 @@ "Mitigation", ) -OPTIONAL_FINDING_FIELDS: tuple[str, ...] = ("Reframe",) +OPTIONAL_FINDING_FIELDS: tuple[str, ...] = ("Reframe", "Question") SUMMARY_MAX_WORDS = 75 SYNTHESIS_MAX_WORDS = 100 FINDING_OBJECTION_MAX_WORDS = 30 FINDING_FIELD_MAX_WORDS = 30 FINDING_REFRAME_MAX_WORDS = 25 +FINDING_QUESTION_MAX_WORDS = 25 TOTAL_MAX_WORDS = 600 MIN_FINDINGS = 1 MAX_FINDINGS = 3 @@ -125,6 +126,7 @@ class ParsedFinding: has_evidence: bool has_mitigation: bool has_reframe: bool + has_question: bool evidence_class: str | None raw_lines: tuple[str, ...] @@ -146,6 +148,7 @@ def parse_findings(findings_body: str) -> list[ParsedFinding]: has_evidence = "**evidence:**" in lower_body has_mitigation = "**mitigation:**" in lower_body has_reframe = "**reframe:**" in lower_body + has_question = "**question:**" in lower_body evidence_class = _extract_evidence_class(body) parsed.append( ParsedFinding( @@ -155,6 +158,7 @@ def parse_findings(findings_body: str) -> list[ParsedFinding]: has_evidence=has_evidence, has_mitigation=has_mitigation, has_reframe=has_reframe, + has_question=has_question, evidence_class=evidence_class, raw_lines=tuple(body.splitlines()), ) @@ -203,6 +207,7 @@ def classify_claim_class(body: str) -> str | None: "ALLOWED_CLAIM_CLASSES", "ALLOWED_OUTCOMES", "Finding", + "FINDING_QUESTION_MAX_WORDS", "OPTIONAL_FINDING_FIELDS", "ParsedFinding", "REQUIRED_FINDING_FIELDS", diff --git a/.github/skills/internal-gateway-critical-master/scripts/validate_critical_output.py b/.github/skills/internal-gateway-critical-master/scripts/validate_critical_output.py index 65cf817c..3c138cd8 100644 --- a/.github/skills/internal-gateway-critical-master/scripts/validate_critical_output.py +++ b/.github/skills/internal-gateway-critical-master/scripts/validate_critical_output.py @@ -21,6 +21,7 @@ ALLOWED_OUTCOMES, FINDING_FIELD_MAX_WORDS, FINDING_OBJECTION_MAX_WORDS, + FINDING_QUESTION_MAX_WORDS, FINDING_REFRAME_MAX_WORDS, MAX_FINDINGS, MIN_FINDINGS, @@ -353,6 +354,21 @@ def _check_finding(parsed, findings: list[Finding]) -> None: extras={"words": words, "limit": FINDING_REFRAME_MAX_WORDS}, ) ) + if parsed.has_question: + words = _field_word_count(parsed.body, "Question") + if words > FINDING_QUESTION_MAX_WORDS: + findings.append( + Finding( + severity="non-blocking", + code="finding-question-word-limit", + path=path, + message=( + f"Question has {words} words; limit is {FINDING_QUESTION_MAX_WORDS}." + ), + suggestion="Shorten the optional root question.", + extras={"words": words, "limit": FINDING_QUESTION_MAX_WORDS}, + ) + ) def _field_word_count(body: str, field_name: str) -> int: diff --git a/tests/test_critical_master_helpers.py b/tests/test_critical_master_helpers.py index b840c8d8..a13aa089 100644 --- a/tests/test_critical_master_helpers.py +++ b/tests/test_critical_master_helpers.py @@ -18,3 +18,16 @@ def test_validate_outcome_value_accepts_allowed_value() -> None: def test_count_words_ignores_fenced_code_blocks() -> None: assert critical_master.count_words("alpha ```python\nbeta\n``` gamma") == 2 + + +def test_parse_findings_detects_optional_root_question() -> None: + findings = critical_master.parse_findings( + "### 1. Audit trail weakens\n\n" + "- **Impact:** Central logs become incomplete.\n" + "- **Evidence:** `inference` - no replacement is described.\n" + "- **Mitigation:** Add a signed attestation.\n" + "- **Question:** Which audit record replaces CI?\n" + ) + + assert len(findings) == 1 + assert findings[0].has_question diff --git a/tests/test_critical_master_validator.py b/tests/test_critical_master_validator.py index 5c0146c1..32e0e862 100644 --- a/tests/test_critical_master_validator.py +++ b/tests/test_critical_master_validator.py @@ -1,5 +1,6 @@ import subprocess import sys +from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path REPO_ROOT = Path(__file__).resolve().parents[1] @@ -8,6 +9,11 @@ / ".github/skills/internal-gateway-critical-master/scripts/validate_critical_output.py" ) FIXTURES = REPO_ROOT / ".github/skills/internal-gateway-critical-master/fixtures" +VALIDATOR_SPEC = spec_from_file_location("validate_critical_output", VALIDATOR) +assert VALIDATOR_SPEC is not None and VALIDATOR_SPEC.loader is not None +VALIDATOR_MODULE = module_from_spec(VALIDATOR_SPEC) +sys.path.insert(0, str(VALIDATOR.parent)) +VALIDATOR_SPEC.loader.exec_module(VALIDATOR_MODULE) def run_validator(*args: str) -> subprocess.CompletedProcess[str]: @@ -43,3 +49,32 @@ def test_strict_mode_fails_on_advisory_finding() -> None: ) assert result.returncode != 0 assert "summary-word-limit" in result.stdout or "total-word-limit" in result.stdout + + +def test_question_word_limit_is_advisory() -> None: + text = """ +## Summary + +We are challenging whether local validation can replace CI validation. + +## Findings + +### 1. The audit trail weakens + +- **Impact:** Central CI logs become incomplete. +- **Evidence:** `inference` - no replacement logging is described. +- **Mitigation:** Define a durable audit record before replacing CI. +- **Question:** Which durable centrally searchable independently retained signed audit record replaces the CI validation log for reviewers, compliance checks, later investigations, audit replay, governance reporting, incident review, and rollout approval? + +## Synthesis + +The strongest risk is compliance visibility. + +## Outcome + +`accept-with-risk` +""" + + findings = VALIDATOR_MODULE.validate_output(text) + + assert any(finding.code == "finding-question-word-limit" for finding in findings) From 1a87b6cac33d8d31b1778a0b2845d416e1636f12 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Mon, 6 Jul 2026 23:16:44 +0200 Subject: [PATCH 07/59] feat: enhance AGENTS.md with test-first red-green-refactor guidelines and exception handling --- .github/skills/internal-tdd/SKILL.md | 45 ++++++++++++++++++++++++---- AGENTS.md | 15 ++++++++++ 2 files changed, 54 insertions(+), 6 deletions(-) diff --git a/.github/skills/internal-tdd/SKILL.md b/.github/skills/internal-tdd/SKILL.md index 6c9d1e53..bbd65724 100644 --- a/.github/skills/internal-tdd/SKILL.md +++ b/.github/skills/internal-tdd/SKILL.md @@ -13,12 +13,16 @@ This index lists every other skill that this file asks the agent to load, route - `superpowers-test-driven-development`: strict red-green-refactor execution when an executable test-first slice is selected. - `superpowers-verification-before-completion`: evidence gate before claiming coverage, fix completion, or red-green-refactor success. -Use this skill as the repository-owned owner for coding changes with executable behavior. Keep the test strategy risk-driven, keep the loop focused on observable behavior, and explain briefly when no new test is useful. +Use this skill as the repository-owned owner for coding changes with executable +behavior. Keep the test strategy risk-driven, keep the loop focused on +observable behavior, and make any no-test path explicit before implementation. ## When to use - Features, bugfixes, or intentional behavior changes with an executable seam. -- Public-interface changes, adapters, tools, modules, validators, or structured outputs whose behavior can be checked through a stable boundary. +- Public-interface changes, adapters, tools, modules, validators, scripts, + CLIs, parsers, sync automation, generators, reports, or structured outputs + whose behavior can be checked through a stable boundary. - Prompt, agent, or LLM-output drift when the change has an executable or evaluable contract with concrete failure examples. - Any coding change where the main failure mode should be identified before implementation and guarded with the lightest useful test-first slice. @@ -32,15 +36,30 @@ Use this skill as the repository-owned owner for coding changes with executable | Level | Use when | Required posture | | --- | --- | --- | -| Mandatory | The change adds or changes executable behavior, or the user explicitly asks for TDD. | Identify the main failure mode, choose the lightest useful guardrail, and write or update the focused failing test first unless no new test is useful. | +| Mandatory | The change adds or changes executable behavior, or the user explicitly asks for TDD. | Identify the main failure mode, choose the lightest useful guardrail, and write or update the focused failing test before the first implementation edit unless a pre-code exception is recorded. | | Recommended | The change touches public boundaries, adapters, modules, structured outputs, or evaluable prompt behavior with meaningful risk. | Prefer contract tests at public boundaries and keep the slice small and reviewable. | | Not suitable | The change is prose-only, prompt-only, skill-only, inventory-only, generated-only, formatting-only, or governance-only with no executable or evaluable contract. | Do not manufacture tests; explain briefly why no new test is useful and use the closest validator or review gate instead. | +## Core Contract + +- `test-first` means the test or check is written or updated before the first + implementation edit. +- An implementation edit is any change to an artifact that creates, changes, or + removes executable or evaluable behavior. +- Tests or checks added after implementation are regression coverage only; do + not describe them as test-first work. +- For changes without a practical stable seam, record a TDD exception note + before implementation. Name the seam gap, the alternate validation path, and + why a focused failing check is not practical. +- If the gate was skipped, stop, disclose the violation, load this contract, + and recover without claiming a retroactive red-green-refactor cycle. + ## Workflow 1. Identify the main failure mode before implementation. 2. Choose the lightest useful guardrail for the active risk. -3. Write or update a focused failing test first for new or changed behavior. +3. Write or update a focused failing test or check before the first + implementation edit for new or changed behavior. 4. Add the regression test before the bug fix when the task is a bugfix. 5. Prefer contract tests at public boundaries over tests of private implementation details. 6. Use small, reviewable golden or snapshot tests only when they are the clearest guardrail. @@ -50,10 +69,21 @@ Use this skill as the repository-owned owner for coding changes with executable ## Test Shape Rules - Test behavior, not implementation details. -- Prefer public APIs, CLIs, generated outputs, or stable validator entrypoints. +- Prefer public APIs, CLIs, generated outputs, stable validator entrypoints, or + machine-readable output contracts. - Mock only external boundaries or expensive collaborators when the real path would make the test slow, flaky, unsafe, or unavailable. - Keep test names in the repository's domain language when one exists. +## Completion States + +- `red-green-refactor`: a focused failing check existed before implementation, + the implementation made it pass, and the closest broader validation ran or + the gap was named. +- `exception-based`: a pre-code TDD exception note exists, and the named + alternate validation ran or the gap was reported. +- `regression-only`: tests or checks were added after implementation. Report + this honestly and do not claim test-first work. + ## Support Skills - `superpowers-test-driven-development`: core red-green-refactor execution when an executable test-first slice is selected. @@ -62,9 +92,12 @@ Use this skill as the repository-owned owner for coding changes with executable ## Validation -- The failing test failed before the fix, or the explicit seam gap is recorded. +- The failing test or check failed before the implementation edit, or the + pre-code TDD exception note is recorded. - The test exercises a public or stable interface. - The implementation is the smallest behavior needed for the active slice. - Focused tests and the closest broader validation pass. - No speculative tests or features were added beyond the requested behavior. +- Completion declares whether the cycle was `red-green-refactor`, + `exception-based`, or `regression-only`. - Use `superpowers-verification-before-completion` before claiming red-green-refactor completion or that a regression is covered. diff --git a/AGENTS.md b/AGENTS.md index 55236726..42a3181e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,6 +79,21 @@ This block is the portable source baseline used to generate - When a contract or policy changes, align the owning tests, validators, or docs instead of letting stale checks restore the old behavior. +## Code Changes + +- Executable or evaluable behavior changes must use a test-first + red-green-refactor loop: define the failing check, make the smallest + implementation edit, then rerun the focused check and closest validation. +- The failing check must exist before the first implementation edit unless a + pre-code testability exception names the gap and alternate validation path. +- Tests added after implementation are regression coverage only; they must not + be represented as test-first work. +- Exceptions are limited to prose-only docs, generated inventory, mechanical + formatting, behavior-neutral renames, read-only validation, or explicit + pre-code testability exceptions. +- If this gate is skipped, agents must stop, disclose the violation, establish + the recovery path, and must not claim retroactive red-green-refactor work. + ## graphify For any question about this repo's architecture, structure, components, or how to add/modify/find From 7c4fe13b11dc91b18bd50263d6609263b35006ef Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Mon, 6 Jul 2026 23:18:36 +0200 Subject: [PATCH 08/59] feat: enhance internal-gateway-idea workflow with new gates and recovery mechanisms --- .github/skills/internal-gateway-idea/SKILL.md | 25 +++++++++++++++++++ .../internal-gateway-idea/agents/openai.yaml | 18 +++++++------ .../references/workflow.md | 12 ++++++--- .../scripts/audit_workflow.py | 9 +++++++ 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/.github/skills/internal-gateway-idea/SKILL.md b/.github/skills/internal-gateway-idea/SKILL.md index 41810312..9693436a 100644 --- a/.github/skills/internal-gateway-idea/SKILL.md +++ b/.github/skills/internal-gateway-idea/SKILL.md @@ -37,6 +37,10 @@ Lightweight repository-owned wrapper for idea shaping. Use `superpowers-brainsto - Keep the `superpowers-brainstorming` hard gate: no implementation action before the user approves the design or direct-plan recommendation. - Treat approval as gate-local. `procedi`, `ok`, `go`, or similar approval advances only the active visible gate. - If approval wording is ambiguous, ask whether it means critical review, retained spec or plan writing, or implementation execution. +- After the bounded evidence pass, run `Idea Gate 0` as a visible numbered question block with `Question`, `Recommendation`, `Why`, and `Default if accepted`; evidence cannot replace Idea Gate 0. +- Do not proceed to assumption challenge, alternative discovery, design direction, critical challenge, or spec-vs-plan decision until `Idea Gate 0` is accepted or the user explicitly overrides its defaults. +- Run `Critical Challenge Gate` as its own visible gate after the user approves the design direction and before the spec-vs-plan decision; an embedded critique does not satisfy Critical Challenge Gate. +- If any mandatory gate was skipped, stop, name the missed gate, mark any downstream artifact as draft-only, and resume at the first skipped mandatory gate. - Use this skill only to add repository-owned idea gates, not to fork the core brainstorming process. - Keep collaborative questioning inside the core brainstorming workflow. - Load `internal-gateway-writing-plans` only after the user approves retained spec or implementation-plan writing. @@ -67,11 +71,32 @@ Follow `references/workflow.md` in this order: 9. `Approved writing handoff` 10. `Stop before implementation execution` +If a later step happened before an earlier mandatory gate, use `Skipped-gate +recovery`: stop the current lane, identify the first skipped mandatory gate, +and resume there before producing or revising a retained artifact. + Do not skip from evidence, design approval, or `Decision: direct plan` to implementation. The only post-brainstorming owner this skill may load is `internal-gateway-writing-plans`, and only after explicit approval for the selected writing path. +## Idea Gate 0 + +Run this gate after bounded evidence and before any challenge or alternative +recommendation. + +Use one visible numbered question block. Each question must include: + +- `Question` +- `Recommendation` +- `Why` +- `Default if accepted` + +Questions should focus only on decisions repository evidence cannot safely +answer: intent, accepted defaults, constraints, success criteria, validation +path, and anti-scope. A bounded evidence pass may prepare recommended defaults, +but evidence cannot replace Idea Gate 0. + ## Assumption Challenge Gate Run this gate before finalizing the design direction. diff --git a/.github/skills/internal-gateway-idea/agents/openai.yaml b/.github/skills/internal-gateway-idea/agents/openai.yaml index 6fb9afdd..8ef9c2b1 100644 --- a/.github/skills/internal-gateway-idea/agents/openai.yaml +++ b/.github/skills/internal-gateway-idea/agents/openai.yaml @@ -7,14 +7,18 @@ interface: $superpowers-brainstorming as the core workflow, then read references/workflow.md for the local state machine. If the incoming request is execution-shaped, emit Specialization Checkpoint: gated and continue - through Idea Gate 0 before any recommendation. Add the local Assumption - Challenge Gate, Alternative discovery, Critical Challenge Gate, and visible - spec-vs-plan decision from SKILL.md. Treat procedi, ok, go, or similar + through Idea Gate 0 before any recommendation. Run Idea Gate 0 as a visible + numbered question block; evidence cannot replace Idea Gate 0. Add the local + Assumption Challenge Gate, Alternative discovery, Critical Challenge Gate, + and visible spec-vs-plan decision from SKILL.md. Run Critical Challenge Gate + as its own visible gate after design-direction approval; an embedded critique + does not satisfy Critical Challenge Gate. Treat procedi, ok, go, or similar approval as advancing only the active visible gate; clarify ambiguous - approval before loading another owner. Load $internal-gateway-writing-plans - only after the user explicitly approves retained spec or implementation-plan - writing. Stop after the writing outcome; do not implement or invoke execution - owners from this skill. Do not retire or rewrite + approval before loading another owner. If a mandatory gate was skipped, stop + and resume at the first skipped mandatory gate. Load + $internal-gateway-writing-plans only after the user explicitly approves + retained spec or implementation-plan writing. Stop after the writing outcome; + do not implement or invoke execution owners from this skill. Do not retire or rewrite internal-gateway-idea-brainstorming during coexistence. name: internal-gateway-idea description: Use when a repository-owned idea needs brainstorming, assumption challenge, alternative discovery, and a spec-vs-plan recommendation before implementation planning. diff --git a/.github/skills/internal-gateway-idea/references/workflow.md b/.github/skills/internal-gateway-idea/references/workflow.md index e80df0b9..b47b342f 100644 --- a/.github/skills/internal-gateway-idea/references/workflow.md +++ b/.github/skills/internal-gateway-idea/references/workflow.md @@ -9,7 +9,10 @@ bundle without changing the current canonical gateway catalog. ```mermaid flowchart TD A[Request enters internal-gateway-idea] --> B[Bounded evidence pass] - B --> C{Concrete execution request?} + B --> X{Mandatory earlier gate skipped?} + X -- yes --> X1[Skipped-gate recovery] + X -- no --> C{Concrete execution request?} + X1 --> D C -- yes --> C1[Specialization Checkpoint: gated] C -- no --> D[Idea Gate 0] C1 --> D @@ -41,10 +44,11 @@ flowchart TD | State | Required behavior | Forbidden behavior | | --- | --- | --- | | `Specialization Checkpoint: gated` | Use when the incoming ask is already a file edit, command run, validator run, implementation step, or other execution-shaped request. Name the later execution owner only as a future consequence. | Do not execute, hand off, or present the post-critical recommendation. | -| `Idea Gate 0` | Confirm the recovered intent, defaults, constraints, success criteria, and validation path. Use the core `superpowers-brainstorming` questioning style. | Do not treat repository evidence alone as user approval. | +| `Skipped-gate recovery` | Stop the current lane, name the first skipped mandatory gate, mark downstream artifacts draft-only, and resume at that gate. | Do not continue from an invalid later state or ask the user to approve a handoff built on skipped gates. | +| `Idea Gate 0` | Confirm the recovered intent, defaults, constraints, success criteria, validation path, and anti-scope with a visible numbered question block using `Question`, `Recommendation`, `Why`, and `Default if accepted`; evidence cannot replace Idea Gate 0. | Do not treat repository evidence alone as user approval, and do not proceed to challenge, alternatives, design, or planning until this gate is accepted. | | `Assumption Challenge Gate` | Test whether the proposed target or solution is necessary before choosing an approach. | Do not only polish the user's proposed solution. | | `Alternative discovery` | Present 2-3 approaches and explain why the recommended one beats the strongest rejected option. | Do not present a single-path design as inevitable. | -| `Critical Challenge Gate` | Challenge the chosen direction before spec or plan writing. Reopen or narrow when the objection is material. | Do not use this gate after loading `internal-gateway-writing-plans`. | +| `Critical Challenge Gate` | Challenge the chosen direction as its own visible gate after design-direction approval and before spec or plan writing. Reopen or narrow when the objection is material. | Do not use this gate after loading `internal-gateway-writing-plans`; an embedded critique does not satisfy Critical Challenge Gate. | | `Spec vs plan decision` | Choose `Decision: direct plan` or `Decision: spec first`, explain why, name the rejected path, and ask for approval. | Do not load `internal-gateway-writing-plans` from the decision alone. | | `Writing outcome only` | Load `internal-gateway-writing-plans` only after explicit user approval for the selected writing path. Stop after the delegated writing outcome. | Do not implement, edit target files, run execution commands, or invoke execution owners. | @@ -53,6 +57,8 @@ flowchart TD - `procedi`, `ok`, `go`, or similar approval advances only the active visible gate. - If the active gate is ambiguous, ask whether the user means critical review, retained spec or plan writing, or implementation execution. +- If a previous mandatory gate was skipped, approval words cannot heal it. Use + `Skipped-gate recovery` and resume at the first skipped mandatory gate. - Approval of a design direction is not approval to implement. - Approval of `Decision: direct plan` skips a retained spec, not the user approval gate and not the stop-before-execution boundary. diff --git a/.github/skills/internal-gateway-idea/scripts/audit_workflow.py b/.github/skills/internal-gateway-idea/scripts/audit_workflow.py index 20e1d29f..27c59b1d 100644 --- a/.github/skills/internal-gateway-idea/scripts/audit_workflow.py +++ b/.github/skills/internal-gateway-idea/scripts/audit_workflow.py @@ -20,9 +20,11 @@ def main() -> int: shared_markers = [ "Specialization Checkpoint: gated", "Idea Gate 0", + "visible numbered question block", "Assumption Challenge Gate", "Alternative discovery", "Critical Challenge Gate", + "embedded critique does not satisfy Critical Challenge Gate", "Spec vs plan decision", "internal-gateway-writing-plans", "Stop before implementation execution", @@ -59,6 +61,13 @@ def main() -> int: "approval_is_gate_local": "active visible gate" in skill_text and "active visible gate" in workflow_text and "active visible gate" in runtime_text, + "evidence_cannot_replace_questions": "evidence cannot replace Idea Gate 0" + in skill_text + and "evidence cannot replace Idea Gate 0" in workflow_text + and "evidence cannot replace Idea Gate 0" in runtime_text, + "skipped_gate_recovery": "first skipped mandatory gate" in skill_text + and "first skipped mandatory gate" in workflow_text + and "first skipped mandatory gate" in runtime_text, "stop_before_execution": "Stop before implementation execution" in workflow_text and "Stop after the delegated writing outcome" in skill_text and "Stop after the writing outcome" in runtime_text, From f70bcd21c33f374919191cd43375acc29a7f4fa3 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Mon, 6 Jul 2026 23:20:03 +0200 Subject: [PATCH 09/59] feat: enhance SKILL.md and related files with Gate Evidence Ledger and clarification handling --- .../internal-gateway-simple-task/SKILL.md | 50 ++++++++-- .../agents/openai.yaml | 2 +- .../references/clarification-gate.md | 3 + .../references/simple-lanes.md | 15 ++- .../references/support-routing.md | 8 +- .../scripts/resolve_simple_task.py | 91 +++++++++++++++++++ 6 files changed, 153 insertions(+), 16 deletions(-) diff --git a/.github/skills/internal-gateway-simple-task/SKILL.md b/.github/skills/internal-gateway-simple-task/SKILL.md index 4ae30518..1bdf649a 100644 --- a/.github/skills/internal-gateway-simple-task/SKILL.md +++ b/.github/skills/internal-gateway-simple-task/SKILL.md @@ -101,17 +101,39 @@ Before operational work, produce a short local brief with no placeholders: This brief must stay shorter than a retained plan. +## Gate Evidence Ledger + +For `full-gate` work, keep a compact ledger before final claims. Each row must be `done`, `skipped`, or `blocked` and must include evidence or a gap. + +Required rows: + +- `bounded-evidence` +- `complexity-cost` +- `initial-idea-ordering` +- `clarification` +- `critical-challenge` +- `readiness-brief` +- `execution` +- `validation` +- `final-evidence` + +Use `skipped` only when the gate policy allows it and the reason is explicit. Use `blocked` when the missing evidence prevents a completion, readiness, passing, fixed, or no-gap claim. + ## Simple Procedure 1. Inspect the nearest local evidence first. 2. Decide whether the task is `trivial-skip`, `full-gate`, or `stop-with-reason`. -3. For non-trivial work, complete Initial Idea Ordering before `grill-me`. -4. Ask one compact `grill-me` block only when it is needed to continue the active lane. -5. Run the critical challenge before non-trivial action. -6. Build the Readiness Brief. -7. Execute the smallest coherent in-scope move. -8. Run focused validation or report the exact validation gap. -9. Use the final evidence gate before positive claims. +3. For `full-gate` work, keep the Gate Evidence Ledger current from the first non-trivial gate onward. +4. For non-trivial work, complete Initial Idea Ordering before `grill-me`. +5. Ask one compact `grill-me` block only when it is needed to continue the active lane. +6. Run the critical challenge before non-trivial action. +7. Build the Readiness Brief. +8. Execute the smallest coherent in-scope move. +9. Run focused validation or report the exact validation gap. +10. Report the ledger summary before strong positive claims. +11. Use the final evidence gate before positive claims. + +For `trivial-skip`, do not create a ledger. Name the validation path directly, or state the exact validation gap. ## Execution Loop @@ -124,6 +146,16 @@ When execution is authorized, iterate with the smallest useful cycle: 5. Continue only while evidence improves. 6. Stop with reason when risk, cost, ambiguity, or validation failure crosses the boundary. +## Simple Code Discipline + +- Task-bound: complete only the concrete requested outcome; stop when target, scope, or validation changes. +- KISS: choose the smallest readable change that solves the observed problem. +- YAGNI: do not add helpers, abstractions, options, configuration, or future proofing without an active task need. +- DRY locally: remove meaningful duplication touched by the task, but do not extract one-off logic. +- Single responsibility: each changed section, helper, or code path must have one current reason to exist. +- Behavior first: when executable behavior changes, name the observable behavior and validate it with the closest stable check. +- Fail fast on drift: stop with reason when the task becomes ambiguous, multi-phase, owner-conflicted, or not locally verifiable. + ## Generic Executable Behavior Rule When executable behavior changes, use a generic test-first loop: @@ -151,5 +183,9 @@ If no useful seam exists, state the seam gap explicitly. - The critical challenge runs before non-trivial action. - Concrete bounded work completes in the same run unless `stop-with-reason` is explicit. - Stop output explains the exact violated condition and required evidence. +- Non-trivial work has a Gate Evidence Ledger entry for each required row, or an explicit blocker. +- Skipped gates record the reason that made the skip valid. +- Blocked gates prevent completion, readiness, passing, fixed, or no-gap claims. +- Executable behavior changes satisfy the Simple Code Discipline. - Executable behavior changes follow the generic test-first loop when a useful seam exists. - Positive claims rely on fresh evidence before completion. diff --git a/.github/skills/internal-gateway-simple-task/agents/openai.yaml b/.github/skills/internal-gateway-simple-task/agents/openai.yaml index cf307d73..a031084e 100644 --- a/.github/skills/internal-gateway-simple-task/agents/openai.yaml +++ b/.github/skills/internal-gateway-simple-task/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Internal Gateway Simple Task" short_description: "Fast path for concrete repository tasks" - default_prompt: "Use $internal-gateway-simple-task for a concrete coding or non-coding task that should finish end-to-end in one bounded run. Start with bounded evidence, local idea ordering, and one compact clarification only when necessary. Use `grill-me` when the task is non-trivial, run `internal-gateway-critical-master` before non-trivial action, stop with reason when the work becomes too large, costly, ambiguous, unsafe, or not locally verifiable, and use `superpowers-verification-before-completion` before positive claims." + default_prompt: "Use $internal-gateway-simple-task for a concrete coding or non-coding task that should finish end-to-end in one bounded run. Start with bounded evidence, local idea ordering, and one compact clarification only when necessary. Use `grill-me` when the task is non-trivial, keep a compact gate evidence ledger for non-trivial work, run `internal-gateway-critical-master` before non-trivial action, stop with reason when the work becomes too large, costly, ambiguous, unsafe, or not locally verifiable, and use `superpowers-verification-before-completion` before positive claims." diff --git a/.github/skills/internal-gateway-simple-task/references/clarification-gate.md b/.github/skills/internal-gateway-simple-task/references/clarification-gate.md index 2ede6696..975b20d7 100644 --- a/.github/skills/internal-gateway-simple-task/references/clarification-gate.md +++ b/.github/skills/internal-gateway-simple-task/references/clarification-gate.md @@ -14,6 +14,9 @@ Skip the gate only with a Trivial-skip proof showing: If a missing answer would change scope, validation, cost, risk, or target state, stop with reason instead of continuing. +Treat `clarification` as `skipped` when local evidence already answers the question or the task is `trivial-skip`. +Treat `clarification` as `blocked` when the missing answer would change scope, validation, cost, risk, or target state. + ## Exit Check Run this check before asking a clarification: diff --git a/.github/skills/internal-gateway-simple-task/references/simple-lanes.md b/.github/skills/internal-gateway-simple-task/references/simple-lanes.md index b9a475a6..aa871825 100644 --- a/.github/skills/internal-gateway-simple-task/references/simple-lanes.md +++ b/.github/skills/internal-gateway-simple-task/references/simple-lanes.md @@ -15,13 +15,13 @@ If cost or complexity exceeds same-run execution, stop and recommend a retained ## Output Shapes -For `answer`, return the answer, evidence, and uncertainty. +For `answer`, return the answer, evidence, uncertainty, and any validation gap when applicable. -For `edit`, return `lane`, `files-touched`, validation, and residual risk. +For `edit`, return `lane`, `files-touched`, `gate-ledger`, validation, and residual risk. -For `diagnose`, return `lane`, reproduced failure, root cause, fix or blocker, and evidence. +For `diagnose`, return `lane`, reproduced failure, root cause, fix or blocker, `gate-ledger`, and evidence. -For `validate`, return `lane`, check, result, and any follow-up gap. +For `validate`, return `lane`, check, result, `gate-ledger`, and any follow-up gap. For `stop-with-reason`, return: @@ -30,6 +30,13 @@ For `stop-with-reason`, return: - `user decision needed` - `evidence required` +## Gate Evidence By Lane + +- `edit`: files touched, behavior or prose contract changed, focused validation command or manual check +- `diagnose`: reproduced failure, hypothesis, falsification check, fix or blocker +- `validate`: command or artifact checked, result, fresh output summary, remaining gap +- `answer`: inspected evidence or explicit uncertainty + ## Multi-Source Mismatch Procedure When the same symptom may come from multiple sources or transformation layers: diff --git a/.github/skills/internal-gateway-simple-task/references/support-routing.md b/.github/skills/internal-gateway-simple-task/references/support-routing.md index f883e197..40bd761c 100644 --- a/.github/skills/internal-gateway-simple-task/references/support-routing.md +++ b/.github/skills/internal-gateway-simple-task/references/support-routing.md @@ -37,11 +37,11 @@ Use these evidence gates before strong claims: | Claim | Evidence gate | | --- | --- | -| `fixed` | Re-run the original failing loop or state the blocker. | -| `covered` | Show the failing-then-passing seam, or state why it could not be run. | +| `fixed` | Mark `validation` done by re-running the original failing loop, or state the blocker. | +| `covered` | Mark `execution` and `validation` done against the failing-then-passing behavior seam, or state why it could not be run. | | `performance-improved` | Compare baseline and after evidence from the same measurement class. | -| `validator-passes` | Re-run the validator and read fresh output before claiming success. | -| `completion`, `readiness`, `no-gap` | Confirm all in-scope work is closed and run the final evidence gate. | +| `validator-passes` | Mark `validation` done by re-running the validator and reading fresh output before claiming success. | +| `completion`, `readiness`, `no-gap` | Mark `final-evidence` done only after all in-scope work is closed and the final evidence gate has fresh support. | If the evidence gate would require broader staged work, stop with reason instead of over-claiming. diff --git a/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py b/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py index 24a2ac13..042f8c75 100644 --- a/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py +++ b/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py @@ -32,6 +32,17 @@ "readiness", "validator-passes", ) +GATE_ROWS = ( + "bounded-evidence", + "complexity-cost", + "initial-idea-ordering", + "clarification", + "critical-challenge", + "readiness-brief", + "execution", + "validation", + "final-evidence", +) CLAIM_REQUIREMENTS = { "fixed": [ @@ -186,6 +197,72 @@ def infer_lane(lane: str, trivial_kind: str | None) -> str: return mapping.get(trivial_kind or "", "unspecified") +def build_gate_evidence( + *, + gate_outcome: str, + validation_path: str, + validation_gap: str, + clarification_required: bool, + stop_reasons: list[str], +) -> list[dict[str, object]]: + validation_evidence = ( + validation_path + or (f"State validation gap: {validation_gap}" if validation_gap else "Name the focused validation path.") + ) + stop_reason_text = ", ".join(stop_reasons) if stop_reasons else "stop reason" + + if gate_outcome == "trivial-skip": + required_rows = {"bounded-evidence", "validation", "final-evidence"} + expected_by_gate = { + "bounded-evidence": "Nearest local evidence proving the task stays tiny and local.", + "complexity-cost": "Not required for trivial-skip.", + "initial-idea-ordering": "Not required for trivial-skip.", + "clarification": "Not required for trivial-skip.", + "critical-challenge": "Not required for trivial-skip.", + "readiness-brief": "Not required for trivial-skip.", + "execution": "Not required for trivial-skip.", + "validation": validation_evidence, + "final-evidence": "Fresh evidence supporting the final claim or the exact remaining gap.", + } + elif gate_outcome == "stop-with-reason": + required_rows = {"bounded-evidence", "complexity-cost"} + expected_by_gate = { + "bounded-evidence": "Nearest local evidence showing why the task cannot stay simple.", + "complexity-cost": f"Blocked reason: {stop_reason_text}.", + "initial-idea-ordering": "Not required after stop-with-reason.", + "clarification": "Not required after stop-with-reason unless the stop reason is a missing answer.", + "critical-challenge": "Not required after stop-with-reason.", + "readiness-brief": "Not required after stop-with-reason.", + "execution": "Not claimable after stop-with-reason; record the blocker instead.", + "validation": validation_evidence, + "final-evidence": "Not claimable after stop-with-reason; record the blocker instead.", + } + else: + expected_by_gate = { + "bounded-evidence": "Nearest local evidence proving the target, scope, and owner.", + "complexity-cost": "Why the task still fits one bounded run.", + "initial-idea-ordering": "Completed local ordering from original request through stop signal.", + "clarification": "Explicit question and answer only if one bounded clarification was needed.", + "critical-challenge": "Fresh challenge outcome recorded before non-trivial action.", + "readiness-brief": "Concrete local readiness brief with scope, validation path, risk, and stop conditions.", + "execution": "Files touched or actions taken, tied to the active lane.", + "validation": validation_evidence, + "final-evidence": "Fresh evidence supporting completion, readiness, passing, fixed, or no-gap claims.", + } + required_rows = set(GATE_ROWS) + if not clarification_required: + required_rows.remove("clarification") + + return [ + { + "gate": gate, + "required": gate in required_rows, + "expected_evidence": expected_by_gate[gate], + } + for gate in GATE_ROWS + ] + + def build_gate_decision( *, task: str, @@ -287,6 +364,13 @@ def build_gate_decision( "stop_conditions": "Stop for complexity, cost, ambiguity, safety, approval, or validation gaps.", "approval": approval, } + gate_evidence = build_gate_evidence( + gate_outcome=gate_outcome, + validation_path=validation_path, + validation_gap=validation_gap, + clarification_required=False, + stop_reasons=stop_reasons, + ) return { "gate_outcome": gate_outcome, @@ -295,6 +379,7 @@ def build_gate_decision( "reason_codes": stop_reasons, "needs_explicit_approval": gate_outcome != "trivial-skip", "readiness_brief": readiness_brief, + "gate_evidence": gate_evidence, } @@ -332,6 +417,12 @@ def render_gate_text(decision: dict[str, object]) -> None: print(f"- Main risk: {brief['main_risk']}") print(f"- Stop conditions: {brief['stop_conditions']}") print(f"- Approval: {brief['approval']}") + print("Gate Evidence:") + for evidence in decision["gate_evidence"]: + print( + f"- {evidence['gate']}: required={str(evidence['required']).lower()}; " + f"expected={evidence['expected_evidence']}" + ) def render_claim_text(claims: list[str], requirements: list[dict[str, str]]) -> None: From 272882ac06cca947d1f6b4797fbc19562e18a319 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Mon, 6 Jul 2026 23:29:22 +0200 Subject: [PATCH 10/59] feat: refine SKILL.md with clearer guidelines on TDD usage and workflow steps --- .github/skills/internal-tdd/SKILL.md | 39 ++++++++++++---------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/.github/skills/internal-tdd/SKILL.md b/.github/skills/internal-tdd/SKILL.md index bbd65724..12cd07f8 100644 --- a/.github/skills/internal-tdd/SKILL.md +++ b/.github/skills/internal-tdd/SKILL.md @@ -9,13 +9,12 @@ description: Use when coding changes have executable behavior, including feature This index lists every other skill that this file asks the agent to load, route to, compare against, or delegate to. -- `internal-debugging`: root-cause diagnosis before regression tests when the failure is not yet understood. -- `superpowers-test-driven-development`: strict red-green-refactor execution when an executable test-first slice is selected. +- `superpowers-test-driven-development`: core red-green-refactor execution after this wrapper selects a mandatory test-first slice. - `superpowers-verification-before-completion`: evidence gate before claiming coverage, fix completion, or red-green-refactor success. Use this skill as the repository-owned owner for coding changes with executable -behavior. Keep the test strategy risk-driven, keep the loop focused on -observable behavior, and make any no-test path explicit before implementation. +behavior. Keep the test strategy risk-driven, focus the loop on observable +behavior, and make any no-test path explicit before implementation. ## When to use @@ -24,7 +23,8 @@ observable behavior, and make any no-test path explicit before implementation. CLIs, parsers, sync automation, generators, reports, or structured outputs whose behavior can be checked through a stable boundary. - Prompt, agent, or LLM-output drift when the change has an executable or evaluable contract with concrete failure examples. -- Any coding change where the main failure mode should be identified before implementation and guarded with the lightest useful test-first slice. +- Any coding change that needs the lightest useful test-first guardrail before + implementation. ## When not to use @@ -36,7 +36,7 @@ observable behavior, and make any no-test path explicit before implementation. | Level | Use when | Required posture | | --- | --- | --- | -| Mandatory | The change adds or changes executable behavior, or the user explicitly asks for TDD. | Identify the main failure mode, choose the lightest useful guardrail, and write or update the focused failing test before the first implementation edit unless a pre-code exception is recorded. | +| Mandatory | The change adds or changes executable behavior, or the user explicitly asks for TDD. | Name the observable behavior or risk, choose the lightest useful guardrail, and write or update the focused failing test before the first implementation edit unless a pre-code exception is recorded. | | Recommended | The change touches public boundaries, adapters, modules, structured outputs, or evaluable prompt behavior with meaningful risk. | Prefer contract tests at public boundaries and keep the slice small and reviewable. | | Not suitable | The change is prose-only, prompt-only, skill-only, inventory-only, generated-only, formatting-only, or governance-only with no executable or evaluable contract. | Do not manufacture tests; explain briefly why no new test is useful and use the closest validator or review gate instead. | @@ -56,15 +56,16 @@ observable behavior, and make any no-test path explicit before implementation. ## Workflow -1. Identify the main failure mode before implementation. -2. Choose the lightest useful guardrail for the active risk. -3. Write or update a focused failing test or check before the first - implementation edit for new or changed behavior. -4. Add the regression test before the bug fix when the task is a bugfix. -5. Prefer contract tests at public boundaries over tests of private implementation details. -6. Use small, reviewable golden or snapshot tests only when they are the clearest guardrail. -7. For prompt, agent, or LLM-output drift, define concrete eval examples or failure cases before changing the implementation. -8. Finish with the closest broader validation and do not weaken, delete, skip, or rewrite tests just to pass. +1. Classify the change as `mandatory`, `recommended`, or `not suitable`. +2. Choose the lightest useful guardrail at the public or stable boundary. +3. If no practical seam exists, record the pre-code TDD exception note before + implementation. +4. When a mandatory test-first slice exists, load + `superpowers-test-driven-development` for the red-green-refactor loop. +5. For prompt, agent, or LLM-output drift, define concrete eval examples or + failure cases before changing the implementation. +6. Finish with the closest broader validation and do not weaken, delete, skip, + or rewrite tests just to pass. ## Test Shape Rules @@ -84,12 +85,6 @@ observable behavior, and make any no-test path explicit before implementation. - `regression-only`: tests or checks were added after implementation. Report this honestly and do not claim test-first work. -## Support Skills - -- `superpowers-test-driven-development`: core red-green-refactor execution when an executable test-first slice is selected. -- `internal-debugging`: root-cause diagnosis before the regression test when the failure is not yet understood. -- `superpowers-verification-before-completion`: evidence gate before claiming coverage, fix completion, or red-green-refactor success. - ## Validation - The failing test or check failed before the implementation edit, or the @@ -100,4 +95,4 @@ observable behavior, and make any no-test path explicit before implementation. - No speculative tests or features were added beyond the requested behavior. - Completion declares whether the cycle was `red-green-refactor`, `exception-based`, or `regression-only`. -- Use `superpowers-verification-before-completion` before claiming red-green-refactor completion or that a regression is covered. +- Final claims passed through `superpowers-verification-before-completion`. From 78f635df2a4f8fd903df1fca092b61c41c594182 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Mon, 6 Jul 2026 23:37:26 +0200 Subject: [PATCH 11/59] feat: update SKILL.md and openai.yaml for clearer TDD guidelines and behavior classification --- .github/skills/internal-tdd/SKILL.md | 104 +++++++++--------- .../skills/internal-tdd/agents/openai.yaml | 4 +- 2 files changed, 53 insertions(+), 55 deletions(-) diff --git a/.github/skills/internal-tdd/SKILL.md b/.github/skills/internal-tdd/SKILL.md index 12cd07f8..574f7b4f 100644 --- a/.github/skills/internal-tdd/SKILL.md +++ b/.github/skills/internal-tdd/SKILL.md @@ -1,6 +1,6 @@ --- name: internal-tdd -description: Use when coding changes have executable behavior, including features, bugfixes, behavior changes, public-interface changes, regression tests, contract seams, or risk-driven test-first guardrails. +description: Use when modifying code with executable or evaluable behavior, including features, bugfixes, behavior changes, public-interface changes, regression tests, contract seams, or risk-driven test-first guardrails. --- # Internal TDD @@ -9,12 +9,16 @@ description: Use when coding changes have executable behavior, including feature This index lists every other skill that this file asks the agent to load, route to, compare against, or delegate to. -- `superpowers-test-driven-development`: core red-green-refactor execution after this wrapper selects a mandatory test-first slice. -- `superpowers-verification-before-completion`: evidence gate before claiming coverage, fix completion, or red-green-refactor success. +- `superpowers-test-driven-development`: core red-green-refactor loop after this wrapper selects a mandatory test-first slice. +- `superpowers-verification-before-completion`: evidence gate before completion, passing, or coverage claims. Use this skill as the repository-owned owner for coding changes with executable -behavior. Keep the test strategy risk-driven, focus the loop on observable -behavior, and make any no-test path explicit before implementation. +or evaluable behavior. It classifies the local seam, names the lightest useful +guardrail, and then delegates mandatory TDD execution to +`superpowers-test-driven-development`. + +This wrapper does not redefine red, green, refactor, test quality, or recovery +mechanics. Those belong to the core skill. ## When to use @@ -22,77 +26,71 @@ behavior, and make any no-test path explicit before implementation. - Public-interface changes, adapters, tools, modules, validators, scripts, CLIs, parsers, sync automation, generators, reports, or structured outputs whose behavior can be checked through a stable boundary. -- Prompt, agent, or LLM-output drift when the change has an executable or evaluable contract with concrete failure examples. -- Any coding change that needs the lightest useful test-first guardrail before - implementation. +- Prompt, agent, or LLM-output drift with concrete failure examples or an + evaluable contract. +- Coding work that needs a repository-local decision about whether TDD is + mandatory, recommended, or not useful. ## When not to use -- Prose-only, prompt-only, skill-only, inventory-only, generated-only, formatting-only, or governance-only edits with no executable or evaluable contract. -- Review-only work; use the review lane separately when the job is defect-first review rather than implementation. -- Mechanical realignment with no behavior change and no credible executable seam. +- Prose-only, prompt-only, skill-only, inventory-only, generated-only, + formatting-only, or governance-only edits with no executable or evaluable + contract. +- Review-only work where the job is defect-first review rather than + implementation. +- Mechanical realignment with no behavior change and no credible executable + seam. ## Applicability Levels | Level | Use when | Required posture | | --- | --- | --- | -| Mandatory | The change adds or changes executable behavior, or the user explicitly asks for TDD. | Name the observable behavior or risk, choose the lightest useful guardrail, and write or update the focused failing test before the first implementation edit unless a pre-code exception is recorded. | -| Recommended | The change touches public boundaries, adapters, modules, structured outputs, or evaluable prompt behavior with meaningful risk. | Prefer contract tests at public boundaries and keep the slice small and reviewable. | -| Not suitable | The change is prose-only, prompt-only, skill-only, inventory-only, generated-only, formatting-only, or governance-only with no executable or evaluable contract. | Do not manufacture tests; explain briefly why no new test is useful and use the closest validator or review gate instead. | +| Mandatory | The change adds or changes executable behavior, or the user explicitly asks for TDD. | Name the observable behavior or risk, choose the smallest useful stable check, then load `superpowers-test-driven-development`. | +| Recommended | The change touches public boundaries, adapters, modules, structured outputs, or evaluable prompt behavior with meaningful risk. | Prefer a public-boundary check and keep the slice small. | +| Not suitable | The change has no practical executable or evaluable contract. | Do not manufacture tests; name the seam gap and use the closest validator or review gate. | ## Core Contract -- `test-first` means the test or check is written or updated before the first - implementation edit. -- An implementation edit is any change to an artifact that creates, changes, or - removes executable or evaluable behavior. -- Tests or checks added after implementation are regression coverage only; do +- Delegate mandatory red-green-refactor work to + `superpowers-test-driven-development`; do not run a local copy of its loop. +- Before implementation, record one of three routing outcomes: + `mandatory`, `recommended`, or `not suitable`. +- For `mandatory`, load the core skill before changing behavior. +- For `recommended`, keep the check at the most meaningful public or stable + boundary. +- For `not suitable`, name the seam gap and the alternate validation path before + implementation. +- Tests or checks added after implementation are regression coverage only. Do not describe them as test-first work. -- For changes without a practical stable seam, record a TDD exception note - before implementation. Name the seam gap, the alternate validation path, and - why a focused failing check is not practical. -- If the gate was skipped, stop, disclose the violation, load this contract, - and recover without claiming a retroactive red-green-refactor cycle. ## Workflow -1. Classify the change as `mandatory`, `recommended`, or `not suitable`. -2. Choose the lightest useful guardrail at the public or stable boundary. -3. If no practical seam exists, record the pre-code TDD exception note before - implementation. -4. When a mandatory test-first slice exists, load - `superpowers-test-driven-development` for the red-green-refactor loop. +1. Identify the observable behavior, risk, or evaluable contract. +2. Pick the closest public or stable boundary that can prove it. +3. Choose `mandatory`, `recommended`, or `not suitable`. +4. Load `superpowers-test-driven-development` only for mandatory TDD execution. 5. For prompt, agent, or LLM-output drift, define concrete eval examples or - failure cases before changing the implementation. -6. Finish with the closest broader validation and do not weaken, delete, skip, - or rewrite tests just to pass. - -## Test Shape Rules - -- Test behavior, not implementation details. -- Prefer public APIs, CLIs, generated outputs, stable validator entrypoints, or - machine-readable output contracts. -- Mock only external boundaries or expensive collaborators when the real path would make the test slow, flaky, unsafe, or unavailable. -- Keep test names in the repository's domain language when one exists. + failure cases before changing behavior. +6. Finish with the focused check, the closest broader validation, and + `superpowers-verification-before-completion` before positive claims. ## Completion States -- `red-green-refactor`: a focused failing check existed before implementation, - the implementation made it pass, and the closest broader validation ran or - the gap was named. -- `exception-based`: a pre-code TDD exception note exists, and the named - alternate validation ran or the gap was reported. +- `red-green-refactor`: the core skill owned the loop and fresh evidence shows + the focused check plus broader validation passed. +- `exception-based`: the seam gap was named before implementation and the + alternate validation ran or its gap was reported. - `regression-only`: tests or checks were added after implementation. Report this honestly and do not claim test-first work. ## Validation -- The failing test or check failed before the implementation edit, or the - pre-code TDD exception note is recorded. -- The test exercises a public or stable interface. -- The implementation is the smallest behavior needed for the active slice. -- Focused tests and the closest broader validation pass. -- No speculative tests or features were added beyond the requested behavior. -- Completion declares whether the cycle was `red-green-refactor`, +- The wrapper chose `mandatory`, `recommended`, or `not suitable` before + implementation. +- Mandatory slices loaded `superpowers-test-driven-development`. +- Not-suitable slices named the seam gap and alternate validation path. +- Focused checks and the closest broader validation ran, or the gap was + reported. +- Completion declares whether the work was `red-green-refactor`, `exception-based`, or `regression-only`. - Final claims passed through `superpowers-verification-before-completion`. diff --git a/.github/skills/internal-tdd/agents/openai.yaml b/.github/skills/internal-tdd/agents/openai.yaml index 1e07b994..0f72161c 100644 --- a/.github/skills/internal-tdd/agents/openai.yaml +++ b/.github/skills/internal-tdd/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Internal TDD" - short_description: "Risk-driven test-first guardrails for executable coding changes" - default_prompt: "Use $internal-tdd for coding changes with executable behavior and choose the lightest useful test-first guardrail before implementation." + short_description: "Risk-driven TDD routing for code changes" + default_prompt: "Use $internal-tdd for coding changes with executable or evaluable behavior; classify the seam, then delegate mandatory red-green-refactor work to $superpowers-test-driven-development." From 32baad9d6319d7e02664eeb7d8e7ee4b4902abab Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Mon, 6 Jul 2026 23:44:48 +0200 Subject: [PATCH 12/59] feat: refactor test_local_agent_sync_install_ai_resources_contracts.py for improved readability and structure --- ...ent_sync_install_ai_resources_contracts.py | 63 ++++++++++++++----- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/tests/test_local_agent_sync_install_ai_resources_contracts.py b/tests/test_local_agent_sync_install_ai_resources_contracts.py index 69953aba..1d65a988 100644 --- a/tests/test_local_agent_sync_install_ai_resources_contracts.py +++ b/tests/test_local_agent_sync_install_ai_resources_contracts.py @@ -15,12 +15,19 @@ sys.path.insert(0, SCRIPT_DIR.as_posix()) from agent_translation import target_extension, translate_agent_for_target # noqa: E402 -from bisync_skills import build_bisync_plan, run_bisync_apply, run_bisync_plan # noqa: E402 -from home_sync_contract import load_home_sync_catalog, load_home_sync_policy # noqa: E402 +from bisync_skills import ( # noqa: E402 + build_bisync_plan, + run_bisync_apply, + run_bisync_plan, +) +from home_sync_contract import ( # noqa: E402 + load_home_sync_catalog, + load_home_sync_policy, +) from home_syncing import ( # noqa: E402 HomeSyncOperation, - build_manifest_payload, build_home_sync_plan, + build_manifest_payload, parse_targets, state_root_for_home, ) @@ -59,7 +66,9 @@ def test_translate_agent_for_codex_preserves_body_and_handoffs(tmp_path: Path) - assert "internal-code-review" in payload["developer_instructions"] -def test_load_home_sync_catalog_autodiscovers_skills_and_honors_policy(tmp_path: Path) -> None: +def test_load_home_sync_catalog_autodiscovers_skills_and_honors_policy( + tmp_path: Path, +) -> None: refs_dir = ( tmp_path / ".github" @@ -165,7 +174,9 @@ def test_build_bisync_plan_filters_local_and_excluded_bundles(tmp_path: Path) -> } assert "bisync-only-home" in plan.blocked_codes assert "bisync-only-repo" in plan.blocked_codes - assert all(drift.skill_name not in {"local-helper", "graphify"} for drift in plan.drifts) + assert all( + drift.skill_name not in {"local-helper", "graphify"} for drift in plan.drifts + ) def test_install_auto_apply_blockers_require_explicit_review() -> None: @@ -310,8 +321,12 @@ def test_fast_mode_does_not_filter_apply_catalog(tmp_path: Path) -> None: encoding="utf-8", ) - normal = build_home_sync_plan(tmp_path, home_root, ("skills",), mode="apply", fast=False) - fast = build_home_sync_plan(tmp_path, home_root, ("skills",), mode="apply", fast=True) + normal = build_home_sync_plan( + tmp_path, home_root, ("skills",), mode="apply", fast=False + ) + fast = build_home_sync_plan( + tmp_path, home_root, ("skills",), mode="apply", fast=True + ) assert fast.source_resources_considered == normal.source_resources_considered == 2 @@ -353,8 +368,12 @@ def test_bisync_apply_requires_reviewed_plan_snapshot( (home_root / ".agents" / "skills").mkdir(parents=True) subprocess.run(["git", "init", "-q"], cwd=repo_root, check=True) - subprocess.run(["git", "config", "user.email", "review@example.com"], cwd=repo_root, check=True) - subprocess.run(["git", "config", "user.name", "reviewer"], cwd=repo_root, check=True) + subprocess.run( + ["git", "config", "user.email", "review@example.com"], cwd=repo_root, check=True + ) + subprocess.run( + ["git", "config", "user.name", "reviewer"], cwd=repo_root, check=True + ) subprocess.run(["git", "add", "."], cwd=repo_root, check=True) subprocess.run(["git", "commit", "-qm", "init"], cwd=repo_root, check=True) @@ -407,8 +426,12 @@ def test_bisync_apply_uses_matching_reviewed_plan_snapshot( (home_root / ".agents" / "skills").mkdir(parents=True) subprocess.run(["git", "init", "-q"], cwd=repo_root, check=True) - subprocess.run(["git", "config", "user.email", "review@example.com"], cwd=repo_root, check=True) - subprocess.run(["git", "config", "user.name", "reviewer"], cwd=repo_root, check=True) + subprocess.run( + ["git", "config", "user.email", "review@example.com"], cwd=repo_root, check=True + ) + subprocess.run( + ["git", "config", "user.name", "reviewer"], cwd=repo_root, check=True + ) subprocess.run(["git", "add", "."], cwd=repo_root, check=True) subprocess.run(["git", "commit", "-qm", "init"], cwd=repo_root, check=True) @@ -512,12 +535,24 @@ def test_cross_target_skill_plan_deduplicates_shared_paths( mode="plan", ) - copy_paths = [operation.path for operation in plan.operations if operation.action == "copy"] - assert copy_paths.count(str((tmp_path / "home" / ".agents" / "skills" / "demo-skill").resolve())) == 1 + copy_paths = [ + operation.path for operation in plan.operations if operation.action == "copy" + ] + assert ( + copy_paths.count( + str((tmp_path / "home" / ".agents" / "skills" / "demo-skill").resolve()) + ) + == 1 + ) manifest_payload = build_manifest_payload(plan) desired_paths = [ resource["target_path"] for resource in manifest_payload["managed_resources"] ] - assert desired_paths.count(str((tmp_path / "home" / ".agents" / "skills" / "demo-skill").resolve())) == 1 + assert ( + desired_paths.count( + str((tmp_path / "home" / ".agents" / "skills" / "demo-skill").resolve()) + ) + == 1 + ) assert plan.source_resources_considered == 1 From 962f104898fdc2c451435fdd972f6a8d79d7452c Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Tue, 7 Jul 2026 11:52:45 +0200 Subject: [PATCH 13/59] feat: update SKILL.md and openai.yaml with timestamped naming conventions for retained artifacts and add tests for filename requirements --- .../internal-gateway-writing-plans/SKILL.md | 7 +++++- .../agents/openai.yaml | 5 +++++ ...internal_gateway_writing_plans_contract.py | 22 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 tests/test_internal_gateway_writing_plans_contract.py diff --git a/.github/skills/internal-gateway-writing-plans/SKILL.md b/.github/skills/internal-gateway-writing-plans/SKILL.md index 5f6f84a3..d8db8073 100644 --- a/.github/skills/internal-gateway-writing-plans/SKILL.md +++ b/.github/skills/internal-gateway-writing-plans/SKILL.md @@ -26,7 +26,12 @@ stops after the delegated outcome. 2. Load `superpowers-writing-plans` and let it create a plan, ask a blocking clarification, redirect, or stop with a reason. Pass an explicit anti-scope and the relevant owners already identified in the preflight so the delegated - plan avoids duplicate or speculative tasks at the source. + plan avoids duplicate or speculative tasks at the source. If the delegated + writing outcome persists a retained artifact, require timestamped local + naming with four-digit 24-hour time: plans use + `tmp/superpowers/plans/YYYY-MM-DD-HHMM-.md` and specs use + `tmp/superpowers/specs/YYYY-MM-DD-HHMM--design.md` such as + `2026-07-03-1143-`. 3. If a retained plan is created, verify execution-readiness and apply Plan Authoring Discipline: ordered tasks, concrete file targets, clear edit intent, validation commands or explicit gaps, stop conditions, and handoff diff --git a/.github/skills/internal-gateway-writing-plans/agents/openai.yaml b/.github/skills/internal-gateway-writing-plans/agents/openai.yaml index de9cca3f..282a42c1 100644 --- a/.github/skills/internal-gateway-writing-plans/agents/openai.yaml +++ b/.github/skills/internal-gateway-writing-plans/agents/openai.yaml @@ -5,6 +5,11 @@ interface: Use $internal-gateway-writing-plans to capture Target, Anti-scope, Nearest owner, Validation path, Stop conditions, and Observable acceptance. Then load $superpowers-writing-plans and delegate the writing outcome. If a + retained artifact is persisted, require timestamped local naming with + `tmp/superpowers/plans/YYYY-MM-DD-HHMM-.md` for plans and + `tmp/superpowers/specs/YYYY-MM-DD-HHMM--design.md` for specs, for + example `2026-07-03-1143-`. + If a retained plan is created, check ordered tasks, concrete file targets, edit intent, validation commands or explicit gaps, duplicate-owner or speculative-scope drift, stop conditions, and handoff readiness. Stop after diff --git a/tests/test_internal_gateway_writing_plans_contract.py b/tests/test_internal_gateway_writing_plans_contract.py new file mode 100644 index 00000000..afbea66e --- /dev/null +++ b/tests/test_internal_gateway_writing_plans_contract.py @@ -0,0 +1,22 @@ +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SKILL_PATH = REPO_ROOT / ".github/skills/internal-gateway-writing-plans/SKILL.md" +AGENT_PATH = ( + REPO_ROOT / ".github/skills/internal-gateway-writing-plans/agents/openai.yaml" +) + + +def test_skill_requires_hhmm_filenames_for_retained_artifacts() -> None: + text = SKILL_PATH.read_text() + + assert "tmp/superpowers/plans/YYYY-MM-DD-HHMM-.md" in text + assert "tmp/superpowers/specs/YYYY-MM-DD-HHMM--design.md" in text + + +def test_agent_prompt_mentions_hhmm_filenames_for_plan_and_spec() -> None: + text = AGENT_PATH.read_text() + + assert "YYYY-MM-DD-HHMM-.md" in text + assert "YYYY-MM-DD-HHMM--design.md" in text From 81064c1fa9b796bccf3ae0c55c9b6085777c0b47 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Tue, 7 Jul 2026 11:56:25 +0200 Subject: [PATCH 14/59] Add Python and Terraform anti-pattern references, enhance internal review high-level skill - Introduced `anti-patterns-python.md` and `anti-patterns-terraform.md` to outline critical, major, minor, and nit anti-patterns for Python and Terraform respectively. - Created `internal-review-high-level` skill to provide systems-level evidence about architecture, workflow, and operational context. - Added detailed checklists for analysis dimensions, audit dispatch, plan completion audit, review lenses, and scope drift to support the new skill. - Updated references and routing for internal review skills to align with the new structure. - Implemented tests for internal gateway simple task and local agent sync install AI resources to ensure proper routing and functionality. --- .github/CHANGELOG.md | 2 +- .github/INVENTORY.md | 6 +- .github/README.md | 2 +- .github/agents/README.md | 4 +- .../agents/internal-gateway-review.agent.md | 4 +- ...agent.md => internal-review-code.agent.md} | 2 +- .github/repo-profiles.yml | 2 +- .github/security-baseline.md | 4 +- .../internal-code-review/agents/openai.yaml | 4 - .github/skills/internal-ddd/SKILL.md | 4 +- .github/skills/internal-debugging/SKILL.md | 10 +-- .github/skills/internal-github-pr/SKILL.md | 8 +- .../agents/openai.yaml | 4 - .../SKILL.md | 12 +-- .../internal-review-code/agents/openai.yaml | 4 + .../references/anti-patterns-bash.md | 0 .../references/anti-patterns-java.md | 0 .../references/anti-patterns-nodejs.md | 0 .../references/anti-patterns-python.md | 0 .../references/anti-patterns-terraform.md | 0 .../SKILL.md | 16 ++-- .../agents/openai.yaml | 4 + .../references/analysis-dimensions.md | 0 .../references/audit-dispatch.md | 0 .../references/plan-completion-audit.md | 0 .../references/review-lenses.md | 4 +- .../references/scope-drift.md | 0 .../references/external-watchlist.yaml | 4 +- .../references/home-sync-catalog.yaml | 4 +- LESSONS_LEARNED.md | 4 +- ...t_internal_gateway_simple_task_contract.py | 79 +++++++++++++++++++ ...ent_sync_install_ai_resources_contracts.py | 4 +- 32 files changed, 135 insertions(+), 56 deletions(-) rename .github/agents/{internal-code-review.agent.md => internal-review-code.agent.md} (99%) delete mode 100644 .github/skills/internal-code-review/agents/openai.yaml delete mode 100644 .github/skills/internal-high-level-review/agents/openai.yaml rename .github/skills/{internal-code-review => internal-review-code}/SKILL.md (96%) create mode 100644 .github/skills/internal-review-code/agents/openai.yaml rename .github/skills/{internal-code-review => internal-review-code}/references/anti-patterns-bash.md (100%) rename .github/skills/{internal-code-review => internal-review-code}/references/anti-patterns-java.md (100%) rename .github/skills/{internal-code-review => internal-review-code}/references/anti-patterns-nodejs.md (100%) rename .github/skills/{internal-code-review => internal-review-code}/references/anti-patterns-python.md (100%) rename .github/skills/{internal-code-review => internal-review-code}/references/anti-patterns-terraform.md (100%) rename .github/skills/{internal-high-level-review => internal-review-high-level}/SKILL.md (96%) create mode 100644 .github/skills/internal-review-high-level/agents/openai.yaml rename .github/skills/{internal-high-level-review => internal-review-high-level}/references/analysis-dimensions.md (100%) rename .github/skills/{internal-high-level-review => internal-review-high-level}/references/audit-dispatch.md (100%) rename .github/skills/{internal-high-level-review => internal-review-high-level}/references/plan-completion-audit.md (100%) rename .github/skills/{internal-high-level-review => internal-review-high-level}/references/review-lenses.md (97%) rename .github/skills/{internal-high-level-review => internal-review-high-level}/references/scope-drift.md (100%) create mode 100644 tests/test_internal_gateway_simple_task_contract.py diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index a7c5c716..346c4f0a 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -133,7 +133,7 @@ Use this format for new updates: - Added `internal-python-runner.sh`, Bash wrappers for every executable repository Python entrypoint, explicit `PyYAML` requirements for `openai-skill-creator`, and wrapper-first invocation guidance across prompts, skills, scripts, security docs, and maintainer workflow references. - Hardened `.github/scripts/validate-copilot-customizations.py`, `tests/test_contract_runner.py`, and `tests/test_validate_copilot_customizations.py` so canonical operational agents must point only to real canonical escalation targets, must not self-route, and stale retired-agent references are caught case-insensitively. - Clarified `internal-router` and `internal-agent-routing-engine` so medium-confidence routing asks at most one targeted clarification question with two clear options before falling back to `internal-planning-leader`. -- Strengthened `.github/skills/internal-code-review/SKILL.md` with a standalone quick-start so the skill stays usable directly as a tactical review asset, not only as the review guard's engine. +- Strengthened `.github/skills/internal-review-code/SKILL.md` with a standalone quick-start so the skill stays usable directly as a tactical review asset, not only as the review guard's engine. - Refreshed `.github/README.md` so the maintainer-facing catalog now matches the live `internal-*`, `obra-*`, and imported support families, the canonical internal agent model, and the actual scripts and workflow present on disk after recent repository restructuring. ## 2026-04-04 diff --git a/.github/INVENTORY.md b/.github/INVENTORY.md index 6689f847..264c584f 100644 --- a/.github/INVENTORY.md +++ b/.github/INVENTORY.md @@ -73,7 +73,7 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/internal-bash/SKILL.md` - `.github/skills/internal-changelog-automation/SKILL.md` - `.github/skills/internal-cloud-policy/SKILL.md` -- `.github/skills/internal-code-review/SKILL.md` +- `.github/skills/internal-review-code/SKILL.md` - `.github/skills/internal-context-handoff/SKILL.md` - `.github/skills/internal-copilot-audit/SKILL.md` - `.github/skills/internal-copilot-docs-research/SKILL.md` @@ -98,7 +98,7 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/internal-github-pr/SKILL.md` - `.github/skills/internal-github-strategic/SKILL.md` - `.github/skills/internal-go/SKILL.md` -- `.github/skills/internal-high-level-review/SKILL.md` +- `.github/skills/internal-review-high-level/SKILL.md` - `.github/skills/internal-java-project/SKILL.md` - `.github/skills/internal-java-spring-boot-development/SKILL.md` - `.github/skills/internal-java/SKILL.md` @@ -176,7 +176,7 @@ These imported `openai-*` office skills remain support-only depth for repositori ## Agents -- `.github/agents/internal-code-review.agent.md` +- `.github/agents/internal-review-code.agent.md` - `.github/agents/internal-gateway-critical-master.agent.md` - `.github/agents/internal-gateway-idea-brainstorming.agent.md` - `.github/agents/internal-gateway-review.agent.md` diff --git a/.github/README.md b/.github/README.md index 5b589070..ad748f65 100644 --- a/.github/README.md +++ b/.github/README.md @@ -13,7 +13,7 @@ customization assets maintained in `cloud-strategy.github`. `internal-gateway-idea-brainstorming`, `internal-gateway-review`, `internal-gateway-critical-master`, `internal-gateway-simple-task` - Specialist repository-owned review agents: - `internal-code-review` for code-focused review before merge or follow-up action. + `internal-review-code` for code-focused review before merge or follow-up action. - Approved `extended` retained plans execute through `internal-gateway-execute-plans`. - When extra provenance helps, offer it as an optional follow-up detail and accept diff --git a/.github/agents/README.md b/.github/agents/README.md index f4a7d136..efb29443 100644 --- a/.github/agents/README.md +++ b/.github/agents/README.md @@ -9,7 +9,7 @@ repo-only sync workflows. critical challenge, and retained planning before execution. - `internal-gateway-review`: owns generic defect-first review for non-code and mixed artifacts before fixes. -- `internal-code-review`: owns dedicated code-focused review for source, tests, +- `internal-review-code`: owns dedicated code-focused review for source, tests, scripts, build metadata, dependency metadata, and code diffs. - `internal-gateway-critical-master`: owns pressure testing. - `internal-gateway-simple-task`: owns concrete execution and approved compact @@ -21,7 +21,7 @@ repo-only sync workflows. | --- | --- | | `internal-gateway-idea-brainstorming` | A vague idea or unresolved goal needs definition and retained planning. | | `internal-gateway-review` | A concrete non-code or mixed artifact needs defect-first review before fixes. | -| `internal-code-review` | A concrete code target needs dedicated code review before merge or follow-up action. | +| `internal-review-code` | A concrete code target needs dedicated code review before merge or follow-up action. | | `internal-gateway-critical-master` | A proposal or plan needs pressure before action. | | `internal-gateway-simple-task` | A concrete low-to-medium-risk task can finish through one focused lane. | diff --git a/.github/agents/internal-gateway-review.agent.md b/.github/agents/internal-gateway-review.agent.md index d9b2018c..f7c06f3f 100644 --- a/.github/agents/internal-gateway-review.agent.md +++ b/.github/agents/internal-gateway-review.agent.md @@ -49,7 +49,7 @@ Evaluate the target through the dimensions that apply to its surface: - **Plans and review packages:** retained plans, specs, audit packages, issue analysis, and decision-support reports. - **Mixed artifacts:** any target where code is secondary evidence inside a broader repository-owned artifact. -Prefer `internal-code-review` when the target is purely code: source, tests, scripts, build files, dependency files, generated-code boundaries, or a code-focused diff. +Prefer `internal-review-code` when the target is purely code: source, tests, scripts, build files, dependency files, generated-code boundaries, or a code-focused diff. ## Critical Counter-Analysis @@ -113,7 +113,7 @@ Use this report shape: - Use this agent when the user asks for review, audit, critique, merge-readiness assessment, prompt or agent review, workflow review, policy review, plan review, or artifact risk assessment. - Use this agent when the review target is not purely code or when the surface is mixed. -- Prefer `internal-code-review` when the requested review is specifically for source code, tests, scripts, build files, dependency files, or a code-focused diff. +- Prefer `internal-review-code` when the requested review is specifically for source code, tests, scripts, build files, dependency files, or a code-focused diff. - Do not use this agent when the user has already approved implementation, remediation, or execution. - Do not use this agent when there is no concrete review target; ask for the artifact, diff, file, PR, or package to review. - Do not delegate to peer agents or hand off to fix lanes. Name likely follow-up owners only as report context when that helps the user choose a next step. diff --git a/.github/agents/internal-code-review.agent.md b/.github/agents/internal-review-code.agent.md similarity index 99% rename from .github/agents/internal-code-review.agent.md rename to .github/agents/internal-review-code.agent.md index b0b2d066..61925eff 100644 --- a/.github/agents/internal-code-review.agent.md +++ b/.github/agents/internal-review-code.agent.md @@ -1,5 +1,5 @@ --- -name: internal-code-review +name: internal-review-code description: "Senior repository code reviewer for source code, tests, scripts, build files, dependency files, and code-focused diffs before merge or follow-up action." tools: ["read", "search", "execute"] disable-model-invocation: true diff --git a/.github/repo-profiles.yml b/.github/repo-profiles.yml index 51cf4e82..5ef230c1 100644 --- a/.github/repo-profiles.yml +++ b/.github/repo-profiles.yml @@ -9,7 +9,7 @@ version: 1 _common_skills: &common_skills - skills/superpowers-brainstorming/SKILL.md - - skills/internal-code-review/SKILL.md + - skills/internal-review-code/SKILL.md - skills/superpowers-dispatching-parallel-agents/SKILL.md - skills/superpowers-executing-plans/SKILL.md - skills/superpowers-finishing-a-development-branch/SKILL.md diff --git a/.github/security-baseline.md b/.github/security-baseline.md index ed92640b..25109b6b 100644 --- a/.github/security-baseline.md +++ b/.github/security-baseline.md @@ -65,8 +65,8 @@ Provide a portable baseline that teams can apply before enabling repository-wide | `shellcheck` on `.github/scripts/` | Automated | pre-commit + CI | | Secret placeholder avoidance in examples/generated artifacts | Partial | pre-commit hooks + review | | OIDC over long-lived secrets | Manual review | `internal-github-actions` | -| IAM least privilege (AWS/Azure/GCP) | Manual review | `internal-code-review` + `internal-terraform` | -| Supply chain hardening | Manual review | `internal-github-actions` + `internal-code-review` | +| IAM least privilege (AWS/Azure/GCP) | Manual review | `internal-review-code` + `internal-terraform` | +| Supply chain hardening | Manual review | `internal-github-actions` + `internal-review-code` | | Branch protection for `.github/**` | Manual review | repository settings requiring `_github-catalog-validation` | | Read-only reviewer agents | Manual review | agent review | | CHANGELOG-based change governance | Manual review | PR review | diff --git a/.github/skills/internal-code-review/agents/openai.yaml b/.github/skills/internal-code-review/agents/openai.yaml deleted file mode 100644 index a3f1fb41..00000000 --- a/.github/skills/internal-code-review/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Internal Code Review" - short_description: "Help with Internal Code Review tasks" - default_prompt: "Use $internal-code-review for this task and follow the repository-owned workflow in the skill." diff --git a/.github/skills/internal-ddd/SKILL.md b/.github/skills/internal-ddd/SKILL.md index 30fe4899..b537b715 100644 --- a/.github/skills/internal-ddd/SKILL.md +++ b/.github/skills/internal-ddd/SKILL.md @@ -10,7 +10,7 @@ description: Use when deciding whether a complex domain needs Domain-Driven Desi This index lists every other skill that this file asks the agent to load, route to, compare against, or delegate to. -- `internal-high-level-review`: incremental adoption review inside existing systems. +- `internal-review-high-level`: incremental adoption review inside existing systems. - `internal-oop-design-patterns`: implementation-level pattern owner after tactical modeling. - `antigravity-api-design-principles`: API or contract design owner when bounded-context seams become interface work. @@ -57,7 +57,7 @@ Use this skill to decide how much Domain-Driven Design a problem deserves, then ## Adjacent lanes -- Use `internal-high-level-review` when the challenge is incremental adoption inside an existing system. +- Use `internal-review-high-level` when the challenge is incremental adoption inside an existing system. - Use `internal-oop-design-patterns` when a tactical model now needs implementation-level pattern choices. - Use `antigravity-api-design-principles` when bounded-context seams are turning into API or contract design work. - Treat infrastructure boundaries as legitimate bounded-context candidates when the real seams are module ownership, environment contracts, or repo-level responsibility lines rather than object-oriented code. diff --git a/.github/skills/internal-debugging/SKILL.md b/.github/skills/internal-debugging/SKILL.md index fbb802e6..74c36731 100644 --- a/.github/skills/internal-debugging/SKILL.md +++ b/.github/skills/internal-debugging/SKILL.md @@ -12,9 +12,9 @@ Treat the referenced skills below as on-demand supports. Do not preload them for every failure loop; load only the owner proved by the active diagnosis, next claim, or validated handoff. -- `internal-code-review`: defect-first review when diagnosis exposes unsafe code or missing tests. +- `internal-review-code`: defect-first review when diagnosis exposes unsafe code or missing tests. - `internal-performance-optimization`: performance owner when measured slowness or throughput is primary. -- `internal-high-level-review`: systems review when diagnosis exposes architecture, workflow, or ownership gaps. +- `internal-review-high-level`: systems review when diagnosis exposes architecture, workflow, or ownership gaps. - `internal-tdd`: regression-test owner after the root cause is understood. - `superpowers-systematic-debugging`: stricter diagnosis discipline for hard, flaky, or guess-prone failures. - `superpowers-verification-before-completion`: evidence gate before claiming the original loop is fixed. @@ -36,7 +36,7 @@ When an unexpected failure appears, stop adjacent feature work, preserve the cur - Use `internal-performance-optimization` when slowness or throughput is the primary problem and a measurable performance question exists. -- Use `internal-code-review` when the task is review-only and no diagnosis loop +- Use `internal-review-code` when the task is review-only and no diagnosis loop is needed. - Use `internal-tdd` when the main job is test-first delivery of new executable behavior, not diagnosing a current failure. @@ -73,9 +73,9 @@ When an unexpected failure appears, stop adjacent feature work, preserve the cur that the bug, failing loop, or drift signal is resolved. - `internal-tdd`: regression tests and public-interface test seams after the root cause is understood. -- `internal-code-review`: defect-first review when the diagnosis exposes a code +- `internal-review-code`: defect-first review when the diagnosis exposes a code smell, missing test, or unsafe fix. -- `internal-high-level-review`: architecture, workflow, or ownership gaps exposed +- `internal-review-high-level`: architecture, workflow, or ownership gaps exposed by missing seams, tangled callers, or cross-boundary drift. - Runtime-specific internal skills: language, workflow, Terraform, GitHub Actions, or script owners for idiomatic fixes and validation. diff --git a/.github/skills/internal-github-pr/SKILL.md b/.github/skills/internal-github-pr/SKILL.md index 30e8b14a..9199d52c 100644 --- a/.github/skills/internal-github-pr/SKILL.md +++ b/.github/skills/internal-github-pr/SKILL.md @@ -12,9 +12,9 @@ Treat the referenced skills below as on-demand supports. Do not preload them for every PR task; load only the owner proved by the touched surface, review need, workflow change, or lifecycle claim. -- `internal-code-review`: defect-first review after PR body or lifecycle work. +- `internal-review-code`: defect-first review after PR body or lifecycle work. - `internal-github-actions`: workflow/action pinning and Actions security rules for PRs that touch workflows. -- `internal-high-level-review`: systems-level impact analysis that feeds PR risk. +- `internal-review-high-level`: systems-level impact analysis that feeds PR risk. - `openai-gh-address-comments`: review-thread remediation after PR body work exists. - `superpowers-verification-before-completion`: evidence gate before claiming PR readiness, validity, mergeability, or completion. @@ -98,8 +98,8 @@ If the user provides a specification, issue, or acceptance outline: ## Cross-references -- **internal-high-level-review**: for systems-level impact analysis that feeds the risk section. -- **internal-code-review**: for the review that follows the PR. +- **internal-review-high-level**: for systems-level impact analysis that feeds the risk section. +- **internal-review-code**: for the review that follows the PR. - **internal-github-actions**: for workflow/action pinning and Actions security rules touched by the PR. - **openai-gh-address-comments**: for addressing review threads and PR comments after the PR body exists; keep review-thread remediation separate from PR lifecycle/body work. - **superpowers-verification-before-completion**: for evidence before readiness, diff --git a/.github/skills/internal-high-level-review/agents/openai.yaml b/.github/skills/internal-high-level-review/agents/openai.yaml deleted file mode 100644 index 2e772463..00000000 --- a/.github/skills/internal-high-level-review/agents/openai.yaml +++ /dev/null @@ -1,4 +0,0 @@ -interface: - display_name: "Internal High-Level Review" - short_description: "Help with internal high-level review tasks" - default_prompt: "Use $internal-high-level-review for this task and follow the repository-owned workflow in the skill." diff --git a/.github/skills/internal-code-review/SKILL.md b/.github/skills/internal-review-code/SKILL.md similarity index 96% rename from .github/skills/internal-code-review/SKILL.md rename to .github/skills/internal-review-code/SKILL.md index d870c607..42ae594e 100644 --- a/.github/skills/internal-code-review/SKILL.md +++ b/.github/skills/internal-review-code/SKILL.md @@ -1,9 +1,9 @@ --- -name: internal-code-review +name: internal-review-code description: Use when review evidence needs line-level or language-specific defect checks for Python, Bash, Terraform, Java, or Node.js/TypeScript code, tests, or scripts. --- -# Code Review Skill +# Internal Review Code ## Referenced skills @@ -12,7 +12,7 @@ This index lists every other skill that this file asks the agent to load, route - `antigravity-golang-pro`: Go concurrency, service design, or performance specialist when Go dominates the review. - `awesome-copilot-codeql`: CodeQL workflow and SARIF specialist when CodeQL setup dominates the review. - `awesome-copilot-secret-scanning`: GitHub secret scanning specialist when secret scanning workflow dominates the review. -- `internal-high-level-review`: systems-level review beyond line-level defects. +- `internal-review-high-level`: systems-level review beyond line-level defects. - `internal-terraform`: Terraform specialist when HCL modeling or drift-safe Terraform dominates the review. - `superpowers-verification-before-completion`: evidence gate before claiming no findings or merge readiness. @@ -135,8 +135,8 @@ Only elevate simplification suggestions when they materially improve maintainabi ## Cross-references -- **internal-high-level-review**: for systems-level impact analysis, architectural evaluation, workflow impact, and blind-spot detection beyond line-level review. -- Use both together: this skill for nit-level anti-patterns first, then `internal-high-level-review` for the bigger picture. +- **internal-review-high-level**: for systems-level impact analysis, architectural evaluation, workflow impact, and blind-spot detection beyond line-level review. +- Use both together: this skill for nit-level anti-patterns first, then `internal-review-high-level` for the bigger picture. - **superpowers-verification-before-completion**: for evidence before claiming no findings, review completion, or merge readiness. @@ -144,7 +144,7 @@ Only elevate simplification suggestions when they materially improve maintainabi Use this skill as the default review owner, then add a narrower specialist only when the evidence demands it. -- Stay with `internal-code-review` when the main need is defect-first review across mixed Python, Bash, Terraform, Java, or Node.js changes. +- Stay with `internal-review-code` when the main need is defect-first review across mixed Python, Bash, Terraform, Java, or Node.js changes. - Add `internal-terraform` when the review is primarily about Terraform resource modeling, module interfaces, or drift-safe HCL changes. - Add `antigravity-golang-pro` when the review is primarily about Go concurrency, service design, or Go performance behavior. - Add `awesome-copilot-codeql` when the review is primarily about CodeQL workflow setup, SARIF handling, or query-suite coverage rather than the diff itself. diff --git a/.github/skills/internal-review-code/agents/openai.yaml b/.github/skills/internal-review-code/agents/openai.yaml new file mode 100644 index 00000000..e45e8fb1 --- /dev/null +++ b/.github/skills/internal-review-code/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Internal Review Code" + short_description: "Help with Internal Review Code tasks" + default_prompt: "Use $internal-review-code for this task and follow the repository-owned workflow in the skill." diff --git a/.github/skills/internal-code-review/references/anti-patterns-bash.md b/.github/skills/internal-review-code/references/anti-patterns-bash.md similarity index 100% rename from .github/skills/internal-code-review/references/anti-patterns-bash.md rename to .github/skills/internal-review-code/references/anti-patterns-bash.md diff --git a/.github/skills/internal-code-review/references/anti-patterns-java.md b/.github/skills/internal-review-code/references/anti-patterns-java.md similarity index 100% rename from .github/skills/internal-code-review/references/anti-patterns-java.md rename to .github/skills/internal-review-code/references/anti-patterns-java.md diff --git a/.github/skills/internal-code-review/references/anti-patterns-nodejs.md b/.github/skills/internal-review-code/references/anti-patterns-nodejs.md similarity index 100% rename from .github/skills/internal-code-review/references/anti-patterns-nodejs.md rename to .github/skills/internal-review-code/references/anti-patterns-nodejs.md diff --git a/.github/skills/internal-code-review/references/anti-patterns-python.md b/.github/skills/internal-review-code/references/anti-patterns-python.md similarity index 100% rename from .github/skills/internal-code-review/references/anti-patterns-python.md rename to .github/skills/internal-review-code/references/anti-patterns-python.md diff --git a/.github/skills/internal-code-review/references/anti-patterns-terraform.md b/.github/skills/internal-review-code/references/anti-patterns-terraform.md similarity index 100% rename from .github/skills/internal-code-review/references/anti-patterns-terraform.md rename to .github/skills/internal-review-code/references/anti-patterns-terraform.md diff --git a/.github/skills/internal-high-level-review/SKILL.md b/.github/skills/internal-review-high-level/SKILL.md similarity index 96% rename from .github/skills/internal-high-level-review/SKILL.md rename to .github/skills/internal-review-high-level/SKILL.md index eed4ccda..db64b1b8 100644 --- a/.github/skills/internal-high-level-review/SKILL.md +++ b/.github/skills/internal-review-high-level/SKILL.md @@ -1,9 +1,9 @@ --- -name: internal-high-level-review +name: internal-review-high-level description: Use when a task needs systems-level evidence about architecture, workflow, cross-cutting impact, blind spots, merge risk, or an orientation map of unfamiliar code. --- -# Internal High-Level Review +# Internal Review High Level ## Referenced skills @@ -12,7 +12,7 @@ Treat the referenced skills below as on-demand supports. Do not preload them for every review; load only the owner proved by the active lens, review scope, or final claim. -- `internal-code-review`: line-level defect review owner. +- `internal-review-code`: line-level defect review owner. - `internal-gateway-critical-master`: pressure-test owner when the main need is challenge rather than review evidence. - `superpowers-verification-before-completion`: evidence gate before claiming no systems findings or merge readiness. @@ -26,13 +26,13 @@ operational context. - Analyze a branch diff, PR, retained plan, or file list for systems-level risk. - Evaluate architectural implications, workflow impact, and unconsidered effects. - Review cross-cutting concerns before merge when line-level findings are not enough. -- Complement `internal-code-review` with broader evidence about coupling, ownership, and operational fit. +- Complement `internal-review-code` with broader evidence about coupling, ownership, and operational fit. - Build a higher-level orientation map for unfamiliar code, including relevant modules, callers, boundaries, and repository domain vocabulary. ## When not to use -- Use `internal-code-review` for line-level defects, language anti-patterns, tests, and file-specific findings. +- Use `internal-review-code` for line-level defects, language anti-patterns, tests, and file-specific findings. - Use `internal-gateway-critical-master` for pre-mortems, hidden-assumption tests, and pressure testing. - Route security-specific gaps through the closest existing owner and state the missing specialized-owner gap when no promoted security owner exists. @@ -43,7 +43,7 @@ operational context. ## Relationship to other skills -- `internal-code-review`: code defects, regressions, tests, language anti-patterns, and file/line findings. +- `internal-review-code`: code defects, regressions, tests, language anti-patterns, and file/line findings. - This skill: architecture, workflow, cross-cutting impact, operational fit, blind spots, and higher-level codebase orientation. - `internal-gateway-critical-master`: challenge work when the main need is pressure testing rather than review evidence. @@ -164,7 +164,7 @@ Do not include empty review sections in an orientation-only answer. | Mistake | Why it matters | Instead | | --- | --- | --- | -| Flagging code style as a systems issue | Inflates findings and dilutes trust | Use `internal-code-review` for nit-level checks | +| Flagging code style as a systems issue | Inflates findings and dilutes trust | Use `internal-review-code` for nit-level checks | | Making ungrounded findings ("this might break X") | Speculation ≠ evidence | Every finding must cite a concrete file and line from the diff | | Scope creep — analyzing the entire codebase | The user asked about a specific change | Analyze only changed files and their immediate dependencies | | Reporting without effort estimation | Leaves the author without prioritization signal | Always include Low/Medium/High effort per finding | @@ -190,7 +190,7 @@ Before presenting findings, verify: is explanatory rather than review-owned. Load `references/analysis-dimensions.md` for detailed checklists. 5. For review-owned work, self-question each finding before including it. -6. Route code defects back to `internal-code-review` when they are not systems-level issues. +6. Route code defects back to `internal-review-code` when they are not systems-level issues. 7. Present review findings or an orientation map using the matching output structure above. diff --git a/.github/skills/internal-review-high-level/agents/openai.yaml b/.github/skills/internal-review-high-level/agents/openai.yaml new file mode 100644 index 00000000..81f8a41f --- /dev/null +++ b/.github/skills/internal-review-high-level/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Internal Review High Level" + short_description: "Help with Internal Review High Level tasks" + default_prompt: "Use $internal-review-high-level for this task and follow the repository-owned workflow in the skill." diff --git a/.github/skills/internal-high-level-review/references/analysis-dimensions.md b/.github/skills/internal-review-high-level/references/analysis-dimensions.md similarity index 100% rename from .github/skills/internal-high-level-review/references/analysis-dimensions.md rename to .github/skills/internal-review-high-level/references/analysis-dimensions.md diff --git a/.github/skills/internal-high-level-review/references/audit-dispatch.md b/.github/skills/internal-review-high-level/references/audit-dispatch.md similarity index 100% rename from .github/skills/internal-high-level-review/references/audit-dispatch.md rename to .github/skills/internal-review-high-level/references/audit-dispatch.md diff --git a/.github/skills/internal-high-level-review/references/plan-completion-audit.md b/.github/skills/internal-review-high-level/references/plan-completion-audit.md similarity index 100% rename from .github/skills/internal-high-level-review/references/plan-completion-audit.md rename to .github/skills/internal-review-high-level/references/plan-completion-audit.md diff --git a/.github/skills/internal-high-level-review/references/review-lenses.md b/.github/skills/internal-review-high-level/references/review-lenses.md similarity index 97% rename from .github/skills/internal-high-level-review/references/review-lenses.md rename to .github/skills/internal-review-high-level/references/review-lenses.md index 5ce2edeb..d84ea793 100644 --- a/.github/skills/internal-high-level-review/references/review-lenses.md +++ b/.github/skills/internal-review-high-level/references/review-lenses.md @@ -14,8 +14,8 @@ persona agents. The wrapper stays thin; each lens maps to an existing skill. | Lens | Trigger | Owner skill | Output | | --- | --- | --- | --- | -| Code defects | Any code, script, config, test, or policy diff under review. | `internal-code-review` | Defect-first findings with file evidence and fix route. | -| Systems fit | Any review that needs workflow, ownership, architecture, or blind-spot evidence. | `internal-high-level-review` | Systems findings, evidence gaps, scope drift, and residual risk. | +| Code defects | Any code, script, config, test, or policy diff under review. | `internal-review-code` | Defect-first findings with file evidence and fix route. | +| Systems fit | Any review that needs workflow, ownership, architecture, or blind-spot evidence. | `internal-review-high-level` | Systems findings, evidence gaps, scope drift, and residual risk. | ## Cross-cutting Lenses diff --git a/.github/skills/internal-high-level-review/references/scope-drift.md b/.github/skills/internal-review-high-level/references/scope-drift.md similarity index 100% rename from .github/skills/internal-high-level-review/references/scope-drift.md rename to .github/skills/internal-review-high-level/references/scope-drift.md diff --git a/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml b/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml index 2cc82df2..4b3c3c61 100644 --- a/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml +++ b/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml @@ -32,12 +32,12 @@ items: action: alert-only - source_family: mattpocock/skills upstream_id: improve-codebase-architecture - local_owner: internal-high-level-review + local_owner: internal-review-high-level reason: Architecture deepening lenses were extracted into high-level review. action: alert-only - source_family: mattpocock/skills upstream_id: zoom-out - local_owner: internal-high-level-review + local_owner: internal-review-high-level reason: Codebase orientation, module maps, caller maps, and domain vocabulary were extracted into high-level review. action: alert-only - source_family: mattpocock/skills diff --git a/.github/skills/local-agent-sync-install-ai-resources/references/home-sync-catalog.yaml b/.github/skills/local-agent-sync-install-ai-resources/references/home-sync-catalog.yaml index c5e7faf4..a3f3337e 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/references/home-sync-catalog.yaml +++ b/.github/skills/local-agent-sync-install-ai-resources/references/home-sync-catalog.yaml @@ -29,9 +29,9 @@ resources: - opencode target_support: See runtime support matrix notes: Gateway agent for non-code and mixed review targets. - - resource_id: internal-code-review + - resource_id: internal-review-code source_family: agents - source_path: .github/agents/internal-code-review.agent.md + source_path: .github/agents/internal-review-code.agent.md include_targets: - codex - copilot diff --git a/LESSONS_LEARNED.md b/LESSONS_LEARNED.md index aca621b0..e3c9a42c 100644 --- a/LESSONS_LEARNED.md +++ b/LESSONS_LEARNED.md @@ -30,8 +30,8 @@ This file retains durable lessons discovered while completing tasks in this repo | 2026-05-01 | After copilot-sync edits Python files, run `pre-commit run ruff-format --all-files` (or push and let CI auto-format) before opening the PR: `ruff-format` will collapse multi-line `logger.error(f"...")` calls onto one line and fail the hook with `files were modified by this hook`. The fix is mechanical, but it surfaces as a red `pre-commit` check until applied. | Pending | `.github/skills/internal-python/SKILL.md` | | 2026-05-01 | When triaging PR checks across many repos, prefer `gh pr checks` text output over `--json name,state,conclusion`: the JSON form silently returns `[]` for some workflow runs (cancelled/queued), while the default text output reliably surfaces the per-check state, duration, and run URL needed for triage. | Pending | `cloud-strategy.github/docs/operations/multi-repo-pr-runbook.md` | | 2026-05-03 | `pull_request_target` workflows use the default-branch workflow definition, so fixing that workflow inside a PR does not change the current PR's run behavior; call out that limitation explicitly when validating PR-local workflow fixes. | Pending | `.github/skills/internal-github-actions/SKILL.md` | -| 2026-05-03 | A local green run is not evidence that a new test or script will pass in CI when it depends on ignored or untracked files, local credentials, or pre-existing workspace state; validate new harnesses from clean-checkout assumptions and verify that required fixtures are tracked and not hidden by `.gitignore`. | Pending | `.github/skills/internal-code-review/SKILL.md` | -| 2026-05-03 | When a shared test fixture changes, update every coupled assertion and checked-in expected output in the same change; otherwise the suite can keep latent failures that only appear after the first blocker is removed. | Pending | `.github/skills/internal-code-review/SKILL.md` | +| 2026-05-03 | A local green run is not evidence that a new test or script will pass in CI when it depends on ignored or untracked files, local credentials, or pre-existing workspace state; validate new harnesses from clean-checkout assumptions and verify that required fixtures are tracked and not hidden by `.gitignore`. | Pending | `.github/skills/internal-review-code/SKILL.md` | +| 2026-05-03 | When a shared test fixture changes, update every coupled assertion and checked-in expected output in the same change; otherwise the suite can keep latent failures that only appear after the first blocker is removed. | Pending | `.github/skills/internal-review-code/SKILL.md` | | 2026-05-03 | For third-party GitHub Actions, configuration options can change required `GITHUB_TOKEN` scopes; if the action documentation says a feature needs higher permissions, either grant that scope explicitly or disable the feature instead of keeping a workflow that fails with `Resource not accessible by integration`. | Pending | `.github/skills/internal-github-actions/SKILL.md` | | 2026-05-03 | For GitHub governance managed with Terraform, "import-safe" cannot be documentation-only: add executable fail-closed guards that keep live management disabled by default, reject `apply` without an explicit live flag, and block plans that would create existing `github_repository` resources before import. | Pending | `.github/skills/internal-terraform/references/structure-standard.md` | | 2026-05-05 | When importing upstream skill files, `fetch_webpage` can return excerpted or reordered page content; verify against exact raw content from `raw.githubusercontent.com` before writing imported assets, then make only the approved local frontmatter adaptation. | Pending | `.github/skills/local-agent-sync-external-resources/SKILL.md` | diff --git a/tests/test_internal_gateway_simple_task_contract.py b/tests/test_internal_gateway_simple_task_contract.py new file mode 100644 index 00000000..e0e7c493 --- /dev/null +++ b/tests/test_internal_gateway_simple_task_contract.py @@ -0,0 +1,79 @@ +import sys +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +SKILL_PATH = REPO_ROOT / ".github/skills/internal-gateway-simple-task/SKILL.md" +AGENT_PATH = ( + REPO_ROOT / ".github/skills/internal-gateway-simple-task/agents/openai.yaml" +) +SUPPORT_ROUTING_PATH = ( + REPO_ROOT + / ".github/skills/internal-gateway-simple-task/references/support-routing.md" +) +RESOLVE_SCRIPT_PATH = ( + REPO_ROOT / ".github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py" +) +SUGGEST_SCRIPT_PATH = ( + REPO_ROOT + / ".github/skills/internal-gateway-simple-task/scripts/suggest_support_skills.py" +) + +RESOLVE_SPEC = spec_from_file_location("resolve_simple_task", RESOLVE_SCRIPT_PATH) +assert RESOLVE_SPEC is not None and RESOLVE_SPEC.loader is not None +resolve_simple_task = module_from_spec(RESOLVE_SPEC) +sys.modules[RESOLVE_SPEC.name] = resolve_simple_task +RESOLVE_SPEC.loader.exec_module(resolve_simple_task) + +SUGGEST_SPEC = spec_from_file_location("suggest_support_skills", SUGGEST_SCRIPT_PATH) +assert SUGGEST_SPEC is not None and SUGGEST_SPEC.loader is not None +suggest_support_skills = module_from_spec(SUGGEST_SPEC) +sys.modules[SUGGEST_SPEC.name] = suggest_support_skills +SUGGEST_SPEC.loader.exec_module(suggest_support_skills) + + +def test_simple_task_bundle_routes_code_changes_to_internal_tdd() -> None: + skill_text = SKILL_PATH.read_text() + agent_text = AGENT_PATH.read_text() + support_routing_text = SUPPORT_ROUTING_PATH.read_text() + + assert "`internal-tdd`" in skill_text + assert "load `internal-tdd` before implementation" in skill_text + assert "load `internal-tdd` before implementation" in agent_text + assert "Load `internal-tdd`" in support_routing_text + + +def test_gate_helper_mentions_internal_tdd_for_executable_behavior() -> None: + decision = resolve_simple_task.build_gate_decision( + task="Update a validator script", + lane="edit", + trivial_kind=None, + prompt="update code path", + depth_keywords=[], + risks=[], + needs_plan=False, + needs_review=False, + needs_critical=False, + owner_ambiguous=False, + clarification_overflow=False, + validation_obvious=True, + validation_path="pytest -q tests/test_example.py", + validation_gap="", + ) + + assert "internal-tdd" in decision["readiness_brief"]["executable_behavior"] + + +def test_support_skill_helper_suggests_internal_tdd_for_code_paths_and_symptom() -> None: + assert suggest_support_skills.SYMPTOM_METHODS["tdd"][0] == "load-internal-tdd" + + suggestions: dict[str, set[str]] = {} + suggest_support_skills.suggest_for_path( + ".github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py", + suggestions, + ) + + assert "bundle-contract-check" in suggestions + assert "load-internal-tdd" in suggestions + assert "runtime-check" in suggestions diff --git a/tests/test_local_agent_sync_install_ai_resources_contracts.py b/tests/test_local_agent_sync_install_ai_resources_contracts.py index 1d65a988..95a079d4 100644 --- a/tests/test_local_agent_sync_install_ai_resources_contracts.py +++ b/tests/test_local_agent_sync_install_ai_resources_contracts.py @@ -47,7 +47,7 @@ def test_translate_agent_for_codex_preserves_body_and_handoffs(tmp_path: Path) - description: Review changes carefully. handoffs: - label: Escalate - agent: internal-code-review + agent: internal-review-code prompt: Include the risky files. --- Main body instructions. @@ -63,7 +63,7 @@ def test_translate_agent_for_codex_preserves_body_and_handoffs(tmp_path: Path) - assert payload["name"] == "review-agent" assert "Main body instructions." in payload["developer_instructions"] assert "## Handoffs" in payload["developer_instructions"] - assert "internal-code-review" in payload["developer_instructions"] + assert "internal-review-code" in payload["developer_instructions"] def test_load_home_sync_catalog_autodiscovers_skills_and_honors_policy( From e4ee27a92fb2ccb187ad6356784dc83940242d43 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Tue, 7 Jul 2026 12:00:43 +0200 Subject: [PATCH 15/59] feat: enhance SKILL.md, openai.yaml, support-routing.md, and scripts for improved TDD routing and behavior handling --- .../internal-gateway-simple-task/SKILL.md | 34 +++++++++++-------- .../agents/openai.yaml | 2 +- .../references/support-routing.md | 2 +- .../scripts/resolve_simple_task.py | 6 ++-- .../scripts/suggest_support_skills.py | 4 +-- 5 files changed, 26 insertions(+), 22 deletions(-) diff --git a/.github/skills/internal-gateway-simple-task/SKILL.md b/.github/skills/internal-gateway-simple-task/SKILL.md index 1bdf649a..3a51f95c 100644 --- a/.github/skills/internal-gateway-simple-task/SKILL.md +++ b/.github/skills/internal-gateway-simple-task/SKILL.md @@ -9,6 +9,7 @@ description: Use when a concrete low-to-medium-risk repository-owned coding or n - `grill-me`: compact Gate 1 interview after local preflight and Initial Idea Ordering. - `internal-gateway-critical-master`: Gate 2 challenge before non-trivial action. +- `internal-tdd`: executable or evaluable behavior changes that need repository-owned TDD routing before implementation. - `superpowers-verification-before-completion`: final evidence gate before completion, readiness, passing, fixed, or no-gap claims. Do not name other skills, agents, or workflow owners from this bundle; when stopping, explain the violated condition and let the user choose the next path. @@ -128,10 +129,12 @@ Use `skipped` only when the gate policy allows it and the reason is explicit. Us 5. Ask one compact `grill-me` block only when it is needed to continue the active lane. 6. Run the critical challenge before non-trivial action. 7. Build the Readiness Brief. -8. Execute the smallest coherent in-scope move. -9. Run focused validation or report the exact validation gap. -10. Report the ledger summary before strong positive claims. -11. Use the final evidence gate before positive claims. +8. When the task changes executable or evaluable behavior, load `internal-tdd` + before implementation and follow its routing outcome. +9. Execute the smallest coherent in-scope move. +10. Run focused validation or report the exact validation gap. +11. Report the ledger summary before strong positive claims. +12. Use the final evidence gate before positive claims. For `trivial-skip`, do not create a ledger. Name the validation path directly, or state the exact validation gap. @@ -154,21 +157,22 @@ When execution is authorized, iterate with the smallest useful cycle: - DRY locally: remove meaningful duplication touched by the task, but do not extract one-off logic. - Single responsibility: each changed section, helper, or code path must have one current reason to exist. - Behavior first: when executable behavior changes, name the observable behavior and validate it with the closest stable check. +- Use `internal-tdd` before implementation when the task changes executable or evaluable behavior and a meaningful seam exists. - Fail fast on drift: stop with reason when the task becomes ambiguous, multi-phase, owner-conflicted, or not locally verifiable. ## Generic Executable Behavior Rule -When executable behavior changes, use a generic test-first loop: +When executable or evaluable behavior changes, route through `internal-tdd` +before implementation: -1. Identify the observable behavior. -2. Choose the smallest useful stable check. -3. Make it fail first when practical. -4. Implement the minimum change. -5. Re-run the focused check. -6. Refactor only after passing. -7. Finish with the closest broader validation. +1. Identify the observable behavior or evaluable contract. +2. Load `internal-tdd` before implementation. +3. Let `internal-tdd` choose `mandatory`, `recommended`, or `not suitable`. +4. Follow the routed check posture and keep the slice small. +5. Finish with the focused check and the closest broader validation. -If no useful seam exists, state the seam gap explicitly. +If `internal-tdd` determines the work is `not suitable`, state the seam gap and +alternate validation path explicitly. ## Deterministic Helpers @@ -178,7 +182,7 @@ If no useful seam exists, state the seam gap explicitly. ## Validation -- Only the three retained skill names appear in this bundle. +- Only the four retained skill names appear in this bundle. - Initial Idea Ordering is completed before `grill-me` for non-trivial work. - The critical challenge runs before non-trivial action. - Concrete bounded work completes in the same run unless `stop-with-reason` is explicit. @@ -187,5 +191,5 @@ If no useful seam exists, state the seam gap explicitly. - Skipped gates record the reason that made the skip valid. - Blocked gates prevent completion, readiness, passing, fixed, or no-gap claims. - Executable behavior changes satisfy the Simple Code Discipline. -- Executable behavior changes follow the generic test-first loop when a useful seam exists. +- Executable or evaluable behavior changes load `internal-tdd` before implementation when a meaningful seam exists. - Positive claims rely on fresh evidence before completion. diff --git a/.github/skills/internal-gateway-simple-task/agents/openai.yaml b/.github/skills/internal-gateway-simple-task/agents/openai.yaml index a031084e..7fcd52f7 100644 --- a/.github/skills/internal-gateway-simple-task/agents/openai.yaml +++ b/.github/skills/internal-gateway-simple-task/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Internal Gateway Simple Task" short_description: "Fast path for concrete repository tasks" - default_prompt: "Use $internal-gateway-simple-task for a concrete coding or non-coding task that should finish end-to-end in one bounded run. Start with bounded evidence, local idea ordering, and one compact clarification only when necessary. Use `grill-me` when the task is non-trivial, keep a compact gate evidence ledger for non-trivial work, run `internal-gateway-critical-master` before non-trivial action, stop with reason when the work becomes too large, costly, ambiguous, unsafe, or not locally verifiable, and use `superpowers-verification-before-completion` before positive claims." + default_prompt: "Use $internal-gateway-simple-task for a concrete coding or non-coding task that should finish end-to-end in one bounded run. Start with bounded evidence, local idea ordering, and one compact clarification only when necessary. If the task changes executable or evaluable behavior, load `internal-tdd` before implementation. Use `grill-me` when the task is non-trivial, keep a compact gate evidence ledger for non-trivial work, run `internal-gateway-critical-master` before non-trivial action, stop with reason when the work becomes too large, costly, ambiguous, unsafe, or not locally verifiable, and use `superpowers-verification-before-completion` before positive claims." diff --git a/.github/skills/internal-gateway-simple-task/references/support-routing.md b/.github/skills/internal-gateway-simple-task/references/support-routing.md index 40bd761c..57ca5c33 100644 --- a/.github/skills/internal-gateway-simple-task/references/support-routing.md +++ b/.github/skills/internal-gateway-simple-task/references/support-routing.md @@ -24,7 +24,7 @@ If no strong signal exists, stay local or stop with reason. | --- | --- | --- | | Missing intent, target path, input data, local context, or one blocker prevents starting | Ask one compact clarification block. | Stop if the answer would change scope, validation, cost, or risk. | | Bug, failing test, failing build, drift, or unexpected output | Reproduce first, then debug by falsifiable hypothesis. | Do not patch from correlation alone. | -| Executable behavior change | Use the generic test-first loop when a meaningful seam exists. | Do not force it onto pure prose or governance wording with no executable seam. | +| Executable behavior change | Load `internal-tdd` to classify `mandatory`, `recommended`, or `not suitable` routing when a meaningful executable or evaluable seam exists. | Do not force it onto pure prose or governance wording with no executable seam. | | Existing diff needs findings or merge-readiness | Stop with reason because the work is no longer simple execution. | Do not turn simple validation into review. | | Orientation or unfamiliar code mapping | Stay descriptive and bounded. | Do not turn orientation into findings without concrete evidence. | | Architecture, workflow, cross-cutting impact, or blind spots dominate | Stop with reason. | Do not keep editing while the boundary is unsettled. | diff --git a/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py b/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py index 042f8c75..0d7cfc9c 100644 --- a/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py +++ b/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py @@ -61,8 +61,8 @@ ], "covered": [ { - "method": "test-first", - "evidence": "Show the failing-then-passing seam, or state why it could not be run.", + "method": "internal-tdd", + "evidence": "Use internal-tdd to show the failing-then-passing seam, or state why it could not be run.", }, { "method": "scope-check", @@ -358,7 +358,7 @@ def build_gate_decision( "anti_scope": "No staged workflow expansion, no hidden delegation, no speculative side work.", "files_expected": "Name only the files proven by local evidence.", "approach": "Use the smallest coherent move that preserves validation coverage.", - "executable_behavior": "Use the generic test-first loop when behavior changes and a useful seam exists.", + "executable_behavior": "Load internal-tdd before implementation when executable or evaluable behavior changes and a useful seam exists.", "validation_path": focused_validation_path, "main_risk": main_risk, "stop_conditions": "Stop for complexity, cost, ambiguity, safety, approval, or validation gaps.", diff --git a/.github/skills/internal-gateway-simple-task/scripts/suggest_support_skills.py b/.github/skills/internal-gateway-simple-task/scripts/suggest_support_skills.py index b2ece7f2..5aa6d7fb 100644 --- a/.github/skills/internal-gateway-simple-task/scripts/suggest_support_skills.py +++ b/.github/skills/internal-gateway-simple-task/scripts/suggest_support_skills.py @@ -16,7 +16,7 @@ "validator-drift": ("diagnose", "Validator drift needs a reproducible loop."), "unexpected": ("diagnose", "Unexpected output needs diagnosis before patching."), "missing-context": ("clarify", "Missing context prevents starting the current lane."), - "tdd": ("test-first", "Executable behavior should be delivered through the smallest useful seam."), + "tdd": ("load-internal-tdd", "Executable behavior should route through internal-tdd before implementation."), "performance": ("measure-performance", "Performance is the primary measured concern."), "pr-readiness": ("lifecycle-check", "Readiness claims need lifecycle evidence before the final answer."), "code-review": ("review-shape-stop", "The work is becoming findings-first rather than execution-first."), @@ -66,7 +66,6 @@ def suggest_for_path(path_text: str, suggestions: dict[str, set[str]]) -> None: if normalized.startswith(".github/skills/internal-") or normalized.startswith(".github/agents/internal-"): add(suggestions, "bundle-contract-check", "Repository-owned bundle paths need contract and local-validator attention.") - return if name == "codeowners": add(suggestions, "governance-check", "Ownership files need governance-aware validation.") return @@ -77,6 +76,7 @@ def suggest_for_path(path_text: str, suggestions: dict[str, set[str]]) -> None: add(suggestions, "automation-check", "Automation files need focused workflow validation.") return if suffix in {".py", ".sh", ".go", ".js", ".cjs", ".mjs", ".ts", ".tsx", ".java"}: + add(suggestions, "load-internal-tdd", "Executable source files should route through internal-tdd before implementation.") add(suggestions, "runtime-check", "Executable source files need the closest runnable or syntax validation.") return if suffix in {".yml", ".yaml", ".json", ".tf"}: From 998c95bebf0b26e865da1b279a3d03aa95cac168 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Wed, 8 Jul 2026 10:25:09 +0200 Subject: [PATCH 16/59] feat: update SKILL.md and tests to refine execution contract and remove unnecessary sections --- .../internal-gateway-simple-task/SKILL.md | 129 ++++++------------ ...t_internal_gateway_simple_task_contract.py | 10 ++ 2 files changed, 53 insertions(+), 86 deletions(-) diff --git a/.github/skills/internal-gateway-simple-task/SKILL.md b/.github/skills/internal-gateway-simple-task/SKILL.md index 3a51f95c..9e42291b 100644 --- a/.github/skills/internal-gateway-simple-task/SKILL.md +++ b/.github/skills/internal-gateway-simple-task/SKILL.md @@ -12,18 +12,20 @@ description: Use when a concrete low-to-medium-risk repository-owned coding or n - `internal-tdd`: executable or evaluable behavior changes that need repository-owned TDD routing before implementation. - `superpowers-verification-before-completion`: final evidence gate before completion, readiness, passing, fixed, or no-gap claims. -Do not name other skills, agents, or workflow owners from this bundle; when stopping, explain the violated condition and let the user choose the next path. +Do not introduce other skills, agents, or workflow owners from this bundle. When stopping, explain the violated condition and let the user choose the next path. -Use this skill as the fast path for concrete bounded work that should finish in the current run. It should answer, edit, diagnose, or validate end-to-end by default when the target, anti-scope, and validation path are concrete enough to execute safely. +## Core Contract -Stop only when the work becomes materially complex, too costly for the current run, ambiguous, unsafe, multi-phase, approval-bound, or not locally verifiable. When stopping, report the exact boundary break instead of delegating by name. +Use this skill as the fast path for concrete bounded work that should finish in the current run. It owns the decision to continue or stop, then answers, edits, diagnoses, or validates end-to-end when the target, anti-scope, and validation path are concrete enough to execute safely. -Use local references and scripts only to keep the decision process compact and deterministic. Keep the working state small, prefer bounded evidence, and preserve direct completion ownership inside this bundle. +Stop only when the work becomes materially complex, too costly for the current run, ambiguous, unsafe, multi-phase, approval-bound, or not locally verifiable. Stop output must explain the boundary break instead of delegating by name. + +Use local references and scripts only to keep the decision process compact and deterministic. Keep working state small, inspect bounded evidence first, and preserve direct completion ownership inside this bundle. ## When to use - The task is concrete and repository-owned. -- The target path, requested outcome, or validation path is already known or can be recovered with one focused clarification. +- The target path, requested outcome, or validation path is known or can be recovered with one focused clarification. - One bounded run can answer, edit, diagnose, or validate the work without staged workflow changes. ## When not to use @@ -33,22 +35,40 @@ Use local references and scripts only to keep the decision process compact and d - The task needs a retained plan, a multi-phase rollout, or separate execution approval. - The task cannot be validated locally or safely with bounded evidence. -## Autonomous Completion Rule +## Gate Decision + +Classify the task before operational work as `trivial-skip`, `full-gate`, or `stop-with-reason`. -Complete the task in the current run when it is concrete, bounded, and locally validatable. Do not stop merely because a supporting method exists. +- Use `trivial-skip` only for a local answer, tiny edit, focused read, or validator run with no material ambiguity, no material risk, and an obvious validation path or explicit validation gap. +- Use `full-gate` for every other same-run task. +- Use `stop-with-reason` when risk, cost, ambiguity, approval, validation, or phase count breaks same-run completion. +- Depth keywords such as `full`, `idea`, and `complete` forbid `trivial-skip`. -When the task cannot safely continue, stop with these fields: +For `stop-with-reason`, report: - `why stopped` - `violated condition` - `user decision needed` - `evidence required before continuing` -Stop output must not name another skill, agent, or owner. +## Execution Contract + +For `trivial-skip`, do not create a ledger. Name the validation path directly, or state the exact validation gap. + +For `full-gate`, complete the gates in this order: -## Initial Idea Ordering +1. Inspect the nearest local evidence. +2. Confirm the task still fits one bounded run. +3. Complete Initial Idea Ordering. +4. Ask one compact `grill-me` block only when a missing bounded fact blocks the active lane. +5. Run the critical challenge before non-trivial action. +6. Write a short Readiness Brief. +7. If executable or evaluable behavior changes, load `internal-tdd` before implementation and follow its routed posture. +8. Execute the smallest coherent in-scope move. +9. Run focused validation or report the exact validation gap. +10. Use the final evidence gate before positive claims. -Complete this local ordering before `grill-me` for any non-trivial task: +Initial Idea Ordering is local to this skill and must cover: - `original request` - `emerged requirements` @@ -60,33 +80,7 @@ Complete this local ordering before `grill-me` for any non-trivial task: - `validation signal` - `stop signal` -This ordering is local to this skill. Do not import it from any other bundle. - -## Gate Policy - -Classify the task before operational work as `trivial-skip`, `full-gate`, or `stop-with-reason`. - -- Use `trivial-skip` only for a local answer, tiny edit, focused read, or validator run with no material ambiguity, no material risk, and an obvious validation path or explicit validation gap. -- Use `full-gate` for everything else that still fits same-run completion. -- Use `stop-with-reason` when the work becomes too complex, too costly, ambiguous, unsafe, approval-bound, multi-phase, or likely incomplete in the current run. - -Gate order: - -1. Bounded evidence. -2. Complexity and cost gate. -3. Initial Idea Ordering. -4. `grill-me` when needed. -5. Critical challenge. -6. Readiness Brief. -7. Execution. -8. Validation. -9. Final evidence gate. - -Depth keywords such as `full`, `idea`, and `complete` forbid `trivial-skip`. - -## Readiness Brief - -Before operational work, produce a short local brief with no placeholders: +The Readiness Brief must stay shorter than a retained plan and include: - `Task` - `Goal` @@ -100,8 +94,6 @@ Before operational work, produce a short local brief with no placeholders: - `Stop conditions` - `Approval` -This brief must stay shorter than a retained plan. - ## Gate Evidence Ledger For `full-gate` work, keep a compact ledger before final claims. Each row must be `done`, `skipped`, or `blocked` and must include evidence or a gap. @@ -120,59 +112,24 @@ Required rows: Use `skipped` only when the gate policy allows it and the reason is explicit. Use `blocked` when the missing evidence prevents a completion, readiness, passing, fixed, or no-gap claim. -## Simple Procedure - -1. Inspect the nearest local evidence first. -2. Decide whether the task is `trivial-skip`, `full-gate`, or `stop-with-reason`. -3. For `full-gate` work, keep the Gate Evidence Ledger current from the first non-trivial gate onward. -4. For non-trivial work, complete Initial Idea Ordering before `grill-me`. -5. Ask one compact `grill-me` block only when it is needed to continue the active lane. -6. Run the critical challenge before non-trivial action. -7. Build the Readiness Brief. -8. When the task changes executable or evaluable behavior, load `internal-tdd` - before implementation and follow its routing outcome. -9. Execute the smallest coherent in-scope move. -10. Run focused validation or report the exact validation gap. -11. Report the ledger summary before strong positive claims. -12. Use the final evidence gate before positive claims. - -For `trivial-skip`, do not create a ledger. Name the validation path directly, or state the exact validation gap. - ## Execution Loop When execution is authorized, iterate with the smallest useful cycle: 1. Confirm the current goal, scope boundary, and next check. -2. Apply the smallest in-scope action. +2. Apply the smallest readable in-scope action. 3. Run the focused validation or evidence check. 4. Repair once when the failure is still in scope and improving. 5. Continue only while evidence improves. 6. Stop with reason when risk, cost, ambiguity, or validation failure crosses the boundary. -## Simple Code Discipline +Keep the implementation posture simple: -- Task-bound: complete only the concrete requested outcome; stop when target, scope, or validation changes. -- KISS: choose the smallest readable change that solves the observed problem. -- YAGNI: do not add helpers, abstractions, options, configuration, or future proofing without an active task need. -- DRY locally: remove meaningful duplication touched by the task, but do not extract one-off logic. -- Single responsibility: each changed section, helper, or code path must have one current reason to exist. -- Behavior first: when executable behavior changes, name the observable behavior and validate it with the closest stable check. -- Use `internal-tdd` before implementation when the task changes executable or evaluable behavior and a meaningful seam exists. -- Fail fast on drift: stop with reason when the task becomes ambiguous, multi-phase, owner-conflicted, or not locally verifiable. - -## Generic Executable Behavior Rule - -When executable or evaluable behavior changes, route through `internal-tdd` -before implementation: - -1. Identify the observable behavior or evaluable contract. -2. Load `internal-tdd` before implementation. -3. Let `internal-tdd` choose `mandatory`, `recommended`, or `not suitable`. -4. Follow the routed check posture and keep the slice small. -5. Finish with the focused check and the closest broader validation. - -If `internal-tdd` determines the work is `not suitable`, state the seam gap and -alternate validation path explicitly. +- Complete only the concrete requested outcome. +- Do not add helpers, abstractions, options, configuration, or future-proofing without an active task need. +- Remove meaningful duplication touched by the task, but do not extract one-off logic. +- Give each changed section, helper, or code path one current reason to exist. +- When behavior changes, name the observable contract and validate it with the closest stable check. ## Deterministic Helpers @@ -182,14 +139,14 @@ alternate validation path explicitly. ## Validation -- Only the four retained skill names appear in this bundle. -- Initial Idea Ordering is completed before `grill-me` for non-trivial work. -- The critical challenge runs before non-trivial action. +- Only the four referenced skill names appear in this bundle. - Concrete bounded work completes in the same run unless `stop-with-reason` is explicit. - Stop output explains the exact violated condition and required evidence. +- `trivial-skip` names the validation path directly or states the exact validation gap. +- Non-trivial work completes Initial Idea Ordering before `grill-me`. +- The critical challenge runs before non-trivial action. - Non-trivial work has a Gate Evidence Ledger entry for each required row, or an explicit blocker. - Skipped gates record the reason that made the skip valid. - Blocked gates prevent completion, readiness, passing, fixed, or no-gap claims. -- Executable behavior changes satisfy the Simple Code Discipline. - Executable or evaluable behavior changes load `internal-tdd` before implementation when a meaningful seam exists. - Positive claims rely on fresh evidence before completion. diff --git a/tests/test_internal_gateway_simple_task_contract.py b/tests/test_internal_gateway_simple_task_contract.py index e0e7c493..79309aa1 100644 --- a/tests/test_internal_gateway_simple_task_contract.py +++ b/tests/test_internal_gateway_simple_task_contract.py @@ -44,6 +44,16 @@ def test_simple_task_bundle_routes_code_changes_to_internal_tdd() -> None: assert "Load `internal-tdd`" in support_routing_text +def test_simple_task_skill_has_one_compact_execution_contract() -> None: + skill_text = SKILL_PATH.read_text() + + assert "## Execution Contract" in skill_text + assert "## Execution Loop" in skill_text + assert "## Simple Code Discipline" not in skill_text + assert "## Generic Executable Behavior Rule" not in skill_text + assert "## Simple Procedure" not in skill_text + + def test_gate_helper_mentions_internal_tdd_for_executable_behavior() -> None: decision = resolve_simple_task.build_gate_decision( task="Update a validator script", From 29b24d06c9ccb77e0fefe167da73488841543228 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Wed, 8 Jul 2026 11:22:04 +0200 Subject: [PATCH 17/59] feat: update SKILL.md and writing-skills-checklist.md for improved skill structure and auditing, add tests for validation --- .../skills/internal-skill-creator/SKILL.md | 1 + .../references/writing-skills-checklist.md | 9 +++++ .../internal-skill-creator/test_contract.py | 33 +++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 tests/.github/skills/internal-skill-creator/test_contract.py diff --git a/.github/skills/internal-skill-creator/SKILL.md b/.github/skills/internal-skill-creator/SKILL.md index ecf1747d..5051fa2e 100644 --- a/.github/skills/internal-skill-creator/SKILL.md +++ b/.github/skills/internal-skill-creator/SKILL.md @@ -111,6 +111,7 @@ Do not restate the full OpenAI creation workflow here. Use this skill to decide - Make descriptions searchable with concrete terms people would actually type: skill, trigger, `.github/skills/`, `SKILL.md`, create, replace, revise, update, reuse, validation. - Preserve a working `description:` during token optimization unless the baseline shows the route itself is the problem. - Keep the body lean. Put only the local contract in `SKILL.md` and move optional depth into references or reusable tools when repeated need justifies it. +- Treat generic skill shape as conditional, not a rigid section template: keep `## Referenced skills` as an audit index, not a preload list; remove duplicated responsibility instead of useful trigger reinforcement; preserve a working `description:` unless routing is the observed failure. - Every repository-owned `SKILL.md` this skill creates or materially revises must keep `## Referenced skills` immediately after the H1. - Treat self-containment as the default for repository-owned skill bundles. Keep required instructions, references, examples, fixtures, scripts, metadata, and diff --git a/.github/skills/internal-skill-creator/references/writing-skills-checklist.md b/.github/skills/internal-skill-creator/references/writing-skills-checklist.md index 1103460f..06a7ef47 100644 --- a/.github/skills/internal-skill-creator/references/writing-skills-checklist.md +++ b/.github/skills/internal-skill-creator/references/writing-skills-checklist.md @@ -15,6 +15,15 @@ This is a local distilled checklist informed by `writing-skills`. It exists to k - Compare the target skill against the closest neighboring owners before deciding the fix. Route quality is a lane-level property. - Keep `## Referenced skills` immediately after the H1 for any created or materially revised `SKILL.md`. +## Generic skill shape + +- Required sections are conditional on behavior, not a rigid template. Frontmatter, H1, trigger-focused `description:`, a clear operating contract, and validation guidance are the stable minimum for material repository-owned skills. +- Conditional sections such as `## Referenced skills`, `## When to use`, `## When not to use`, local references, scripts, examples, and fixtures should appear only when they improve routing, boundary clarity, portability, or maintenance. +- Do not require every section for every skill. A small single-owner skill should stay small when extra sections would only repeat the trigger or body. +- Treat `## Referenced skills` as an audit index, not a preload list. Do not load referenced skills from this section alone; load another skill only when the active task, file, framework, runtime, blocker, validation path, or explicit user request proves that owner is needed. +- Remove duplicated responsibility, not useful trigger reinforcement. Keep short repeated safety or retrieval language when it protects activation, stop conditions, or claim discipline. +- Preserve a working `description:` during cleanup unless the observed baseline failure is routing itself. + ## Discovery and retrieval - Keep `description:` focused on when the skill should load. diff --git a/tests/.github/skills/internal-skill-creator/test_contract.py b/tests/.github/skills/internal-skill-creator/test_contract.py new file mode 100644 index 00000000..8eca4d25 --- /dev/null +++ b/tests/.github/skills/internal-skill-creator/test_contract.py @@ -0,0 +1,33 @@ +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[4] +SKILL_PATH = REPO_ROOT / ".github/skills/internal-skill-creator/SKILL.md" +CHECKLIST_PATH = ( + REPO_ROOT + / ".github/skills/internal-skill-creator/references/writing-skills-checklist.md" +) + + +def test_referenced_skills_are_audit_index_not_preload() -> None: + skill_text = SKILL_PATH.read_text() + checklist_text = CHECKLIST_PATH.read_text() + + assert "audit index, not a preload" in skill_text + assert "audit index, not a preload" in checklist_text + assert "Do not load referenced skills from this section alone" in checklist_text + + +def test_generic_skill_shape_is_conditional_not_rigid() -> None: + checklist_text = CHECKLIST_PATH.read_text() + + assert "## Generic skill shape" in checklist_text + assert "Conditional sections" in checklist_text + assert "Do not require every section for every skill" in checklist_text + + +def test_skill_cleanup_preserves_triggers_and_removes_responsibility_duplication() -> None: + checklist_text = CHECKLIST_PATH.read_text() + + assert "Remove duplicated responsibility, not useful trigger reinforcement" in checklist_text + assert "Preserve a working `description:` during cleanup" in checklist_text From 42e1db91586f25dd8e2d80459d6e65d36aff805a Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Wed, 8 Jul 2026 12:11:36 +0200 Subject: [PATCH 18/59] feat: add tests for internal skill contracts, improve test layout, and enhance anti-pattern documentation --- .../references/anti-patterns-python.md | 2 +- INTERNAL_CONTRACT.md | 15 +++++ .../scripts}/test_run_sh_dispatch.py | 6 +- .../scripts}/test_critical_master_helpers.py | 11 +++- .../test_critical_master_validator.py | 6 +- .../test_simple_task_contract.py} | 6 +- .../test_writing_plans_contract.py} | 6 +- .../test_internal_skill_creator_contract.py} | 6 +- .../scripts/test_contracts.py} | 18 +++--- .../scripts/test_sync_output.py} | 11 +++- tests/test_repository_test_layout_contract.py | 56 +++++++++++++++++++ 11 files changed, 124 insertions(+), 19 deletions(-) rename tests/{ => github/scripts}/test_run_sh_dispatch.py (87%) rename tests/{ => github/skills/internal-gateway-critical-master/scripts}/test_critical_master_helpers.py (80%) rename tests/{ => github/skills/internal-gateway-critical-master/scripts}/test_critical_master_validator.py (94%) rename tests/{test_internal_gateway_simple_task_contract.py => github/skills/internal-gateway-simple-task/test_simple_task_contract.py} (95%) rename tests/{test_internal_gateway_writing_plans_contract.py => github/skills/internal-gateway-writing-plans/test_writing_plans_contract.py} (81%) rename tests/{.github/skills/internal-skill-creator/test_contract.py => github/skills/internal-skill-creator/test_internal_skill_creator_contract.py} (88%) rename tests/{test_local_agent_sync_install_ai_resources_contracts.py => github/skills/local-agent-sync-install-ai-resources/scripts/test_contracts.py} (97%) rename tests/{test_home_ai_sync_output.py => github/skills/local-agent-sync-install-ai-resources/scripts/test_sync_output.py} (87%) create mode 100644 tests/test_repository_test_layout_contract.py diff --git a/.github/skills/internal-review-code/references/anti-patterns-python.md b/.github/skills/internal-review-code/references/anti-patterns-python.md index 9b2473e4..10cc6c0b 100644 --- a/.github/skills/internal-review-code/references/anti-patterns-python.md +++ b/.github/skills/internal-review-code/references/anti-patterns-python.md @@ -22,7 +22,7 @@ Baseline owner: `internal-python` | PY-M06 | Cyclomatic complexity > 10 per function | Hard to test and maintain | | PY-M07 | `print()` instead of `logging` in application/library code | No log level control in production | | PY-M08 | Missing unit tests for new public functions | Violates test coverage mandate | -| PY-M09 | Python tests outside repository-root `tests/` or without mirrored source paths | Breaks repository test discoverability and ownership mapping | +| PY-M09 | Python tests outside repository-root `tests/` or without discoverable mirrored source paths | Breaks repository test discoverability and ownership mapping | | PY-M10 | `rich`, emoji, tables, or panels outside human-facing CLI/reporting boundaries | Mixes terminal UI with importable logic or machine-readable output such as JSON | ## Minor diff --git a/INTERNAL_CONTRACT.md b/INTERNAL_CONTRACT.md index ab740b16..f244f2cd 100644 --- a/INTERNAL_CONTRACT.md +++ b/INTERNAL_CONTRACT.md @@ -196,6 +196,21 @@ Treat the current skill-first architecture as the source of truth. Do not infer - when historical context is retained, it is clearly marked as historical rather than normative - future automation is rebuilt from the current contract, not from stale references +### Test Layout + +#### `testing-python-tests-mirror-source-layout` + +- Goal: keep Python test ownership obvious from the test path alone. +- Scope: + - repository-root `tests/` + - Python tests that target nested repository-owned source paths +- Expected behavior: + - Python tests remain under repository-root `tests/` + - when a test targets a nested source path, the test path under `tests/` mirrors the owned source directory closely enough that the tested folder is obvious + - when the tested source path starts with a hidden repository segment such as `.github/`, the mirrored test path uses a discoverable directory such as `tests/github/...` instead of a hidden path that pytest may skip + - flat root-level files such as `tests/test_*.py` are reserved for root-owned contracts or sources that also live at the repository root + - tests must not hide skill-owned or script-owned coverage behind unrelated flat filenames when a mirrored path would make the owner clear + ### Naming And Operating Model #### `resource-governance-uses-supported-origin-naming` diff --git a/tests/test_run_sh_dispatch.py b/tests/github/scripts/test_run_sh_dispatch.py similarity index 87% rename from tests/test_run_sh_dispatch.py rename to tests/github/scripts/test_run_sh_dispatch.py index eeaadd45..f33ab81e 100644 --- a/tests/test_run_sh_dispatch.py +++ b/tests/github/scripts/test_run_sh_dispatch.py @@ -1,7 +1,11 @@ import subprocess from pathlib import Path -REPO_ROOT = Path(__file__).resolve().parents[1] +REPO_ROOT = next( + parent + for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) def run_shell(command: str) -> subprocess.CompletedProcess[str]: diff --git a/tests/test_critical_master_helpers.py b/tests/github/skills/internal-gateway-critical-master/scripts/test_critical_master_helpers.py similarity index 80% rename from tests/test_critical_master_helpers.py rename to tests/github/skills/internal-gateway-critical-master/scripts/test_critical_master_helpers.py index a13aa089..1ea5fa07 100644 --- a/tests/test_critical_master_helpers.py +++ b/tests/github/skills/internal-gateway-critical-master/scripts/test_critical_master_helpers.py @@ -2,9 +2,14 @@ from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path -MODULE_PATH = Path( - ".github/skills/internal-gateway-critical-master/scripts/critical_master.py" -).resolve() +REPO_ROOT = next( + parent + for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +MODULE_PATH = ( + REPO_ROOT / ".github/skills/internal-gateway-critical-master/scripts/critical_master.py" +) SPEC = spec_from_file_location("critical_master", MODULE_PATH) assert SPEC is not None and SPEC.loader is not None critical_master = module_from_spec(SPEC) diff --git a/tests/test_critical_master_validator.py b/tests/github/skills/internal-gateway-critical-master/scripts/test_critical_master_validator.py similarity index 94% rename from tests/test_critical_master_validator.py rename to tests/github/skills/internal-gateway-critical-master/scripts/test_critical_master_validator.py index 32e0e862..bc7df772 100644 --- a/tests/test_critical_master_validator.py +++ b/tests/github/skills/internal-gateway-critical-master/scripts/test_critical_master_validator.py @@ -3,7 +3,11 @@ from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path -REPO_ROOT = Path(__file__).resolve().parents[1] +REPO_ROOT = next( + parent + for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) VALIDATOR = ( REPO_ROOT / ".github/skills/internal-gateway-critical-master/scripts/validate_critical_output.py" diff --git a/tests/test_internal_gateway_simple_task_contract.py b/tests/github/skills/internal-gateway-simple-task/test_simple_task_contract.py similarity index 95% rename from tests/test_internal_gateway_simple_task_contract.py rename to tests/github/skills/internal-gateway-simple-task/test_simple_task_contract.py index 79309aa1..eedbd11a 100644 --- a/tests/test_internal_gateway_simple_task_contract.py +++ b/tests/github/skills/internal-gateway-simple-task/test_simple_task_contract.py @@ -3,7 +3,11 @@ from pathlib import Path -REPO_ROOT = Path(__file__).resolve().parents[1] +REPO_ROOT = next( + parent + for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) SKILL_PATH = REPO_ROOT / ".github/skills/internal-gateway-simple-task/SKILL.md" AGENT_PATH = ( REPO_ROOT / ".github/skills/internal-gateway-simple-task/agents/openai.yaml" diff --git a/tests/test_internal_gateway_writing_plans_contract.py b/tests/github/skills/internal-gateway-writing-plans/test_writing_plans_contract.py similarity index 81% rename from tests/test_internal_gateway_writing_plans_contract.py rename to tests/github/skills/internal-gateway-writing-plans/test_writing_plans_contract.py index afbea66e..493a4b17 100644 --- a/tests/test_internal_gateway_writing_plans_contract.py +++ b/tests/github/skills/internal-gateway-writing-plans/test_writing_plans_contract.py @@ -1,7 +1,11 @@ from pathlib import Path -REPO_ROOT = Path(__file__).resolve().parents[1] +REPO_ROOT = next( + parent + for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) SKILL_PATH = REPO_ROOT / ".github/skills/internal-gateway-writing-plans/SKILL.md" AGENT_PATH = ( REPO_ROOT / ".github/skills/internal-gateway-writing-plans/agents/openai.yaml" diff --git a/tests/.github/skills/internal-skill-creator/test_contract.py b/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py similarity index 88% rename from tests/.github/skills/internal-skill-creator/test_contract.py rename to tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py index 8eca4d25..b5be5f87 100644 --- a/tests/.github/skills/internal-skill-creator/test_contract.py +++ b/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py @@ -1,7 +1,11 @@ from pathlib import Path -REPO_ROOT = Path(__file__).resolve().parents[4] +REPO_ROOT = next( + parent + for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) SKILL_PATH = REPO_ROOT / ".github/skills/internal-skill-creator/SKILL.md" CHECKLIST_PATH = ( REPO_ROOT diff --git a/tests/test_local_agent_sync_install_ai_resources_contracts.py b/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_contracts.py similarity index 97% rename from tests/test_local_agent_sync_install_ai_resources_contracts.py rename to tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_contracts.py index 95a079d4..92367f7b 100644 --- a/tests/test_local_agent_sync_install_ai_resources_contracts.py +++ b/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_contracts.py @@ -9,9 +9,14 @@ import pytest -SCRIPT_DIR = Path( - ".github/skills/local-agent-sync-install-ai-resources/scripts" -).resolve() +REPO_ROOT = next( + parent + for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +SCRIPT_DIR = ( + REPO_ROOT / ".github/skills/local-agent-sync-install-ai-resources/scripts" +) sys.path.insert(0, SCRIPT_DIR.as_posix()) from agent_translation import target_extension, translate_agent_for_target # noqa: E402 @@ -225,10 +230,9 @@ def test_bisync_requires_review_only_for_non_safe_drift() -> None: def test_skill_run_sh_should_quiet_only_for_compact_modes() -> None: run_sh = ( - Path(".github/skills/local-agent-sync-install-ai-resources/scripts/run.sh") - .resolve() - .as_posix() - ) + REPO_ROOT + / ".github/skills/local-agent-sync-install-ai-resources/scripts/run.sh" + ).resolve().as_posix() script = textwrap.dedent( f"""\ source "{run_sh}" diff --git a/tests/test_home_ai_sync_output.py b/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_sync_output.py similarity index 87% rename from tests/test_home_ai_sync_output.py rename to tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_sync_output.py index ec4dbd6a..7d02ec5d 100644 --- a/tests/test_home_ai_sync_output.py +++ b/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_sync_output.py @@ -2,9 +2,14 @@ import sys from pathlib import Path -SCRIPT_DIR = Path( - ".github/skills/local-agent-sync-install-ai-resources/scripts" -).resolve() +REPO_ROOT = next( + parent + for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +SCRIPT_DIR = ( + REPO_ROOT / ".github/skills/local-agent-sync-install-ai-resources/scripts" +) sys.path.insert(0, SCRIPT_DIR.as_posix()) from sync_output import build_compact_install_output, dump_compact_json # noqa: E402 diff --git a/tests/test_repository_test_layout_contract.py b/tests/test_repository_test_layout_contract.py new file mode 100644 index 00000000..e16503d6 --- /dev/null +++ b/tests/test_repository_test_layout_contract.py @@ -0,0 +1,56 @@ +import re +from pathlib import Path + + +REPO_ROOT = next( + parent + for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +TESTS_ROOT = REPO_ROOT / "tests" +INTERNAL_CONTRACT = REPO_ROOT / "INTERNAL_CONTRACT.md" +ANTI_PATTERNS = ( + REPO_ROOT / ".github/skills/internal-review-code/references/anti-patterns-python.md" +) +SKILL_PATH_PATTERN = re.compile(r"\.github/skills/([^/]+)/") + + +def test_contract_documents_discoverable_mirrored_test_paths() -> None: + contract_text = INTERNAL_CONTRACT.read_text(encoding="utf-8") + anti_patterns_text = ANTI_PATTERNS.read_text(encoding="utf-8") + + assert "testing-python-tests-mirror-source-layout" in contract_text + assert "tests/github/..." in contract_text + assert "flat root-level files such as `tests/test_*.py` are reserved" in contract_text + assert "discoverable mirrored source paths" in anti_patterns_text + + +def test_github_owned_python_tests_live_in_mirrored_paths() -> None: + violations: list[str] = [] + + for test_path in sorted(TESTS_ROOT.rglob("test_*.py")): + if test_path.name == "test_repository_test_layout_contract.py": + continue + + rel_path = test_path.relative_to(TESTS_ROOT) + text = test_path.read_text(encoding="utf-8") + + if ".github/scripts/run.sh" in text: + if rel_path.parts[:2] != ("github", "scripts"): + violations.append( + f"{rel_path} should live under tests/github/scripts/ for .github/scripts coverage" + ) + continue + + skill_match = SKILL_PATH_PATTERN.search(text) + if skill_match is None: + continue + + expected_prefix = ("github", "skills", skill_match.group(1)) + if rel_path.parts[:3] != expected_prefix: + expected = "/".join(expected_prefix) + violations.append( + f"{rel_path} should live under tests/{expected}/ for .github/skills/{skill_match.group(1)}/ coverage" + ) + + assert not violations, "\n".join(violations) From cb3cd683eaa64a7d08dc3fba8f44eabbdb7a14cb Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Wed, 8 Jul 2026 12:15:05 +0200 Subject: [PATCH 19/59] feat: refine guidance on test placement and ownership clarity in documentation --- .../references/anti-patterns-python.md | 2 +- .github/skills/internal-tdd/SKILL.md | 3 ++ AGENTS.md | 9 ++++-- INTERNAL_CONTRACT.md | 8 +++--- tests/test_repository_test_layout_contract.py | 28 +++++++++++-------- 5 files changed, 31 insertions(+), 19 deletions(-) diff --git a/.github/skills/internal-review-code/references/anti-patterns-python.md b/.github/skills/internal-review-code/references/anti-patterns-python.md index 10cc6c0b..08966380 100644 --- a/.github/skills/internal-review-code/references/anti-patterns-python.md +++ b/.github/skills/internal-review-code/references/anti-patterns-python.md @@ -22,7 +22,7 @@ Baseline owner: `internal-python` | PY-M06 | Cyclomatic complexity > 10 per function | Hard to test and maintain | | PY-M07 | `print()` instead of `logging` in application/library code | No log level control in production | | PY-M08 | Missing unit tests for new public functions | Violates test coverage mandate | -| PY-M09 | Python tests outside repository-root `tests/` or without discoverable mirrored source paths | Breaks repository test discoverability and ownership mapping | +| PY-M09 | Python tests outside repository-root `tests/` or without paths that make the covered owner or checked behavior obvious | Breaks repository test discoverability and ownership mapping | | PY-M10 | `rich`, emoji, tables, or panels outside human-facing CLI/reporting boundaries | Mixes terminal UI with importable logic or machine-readable output such as JSON | ## Minor diff --git a/.github/skills/internal-tdd/SKILL.md b/.github/skills/internal-tdd/SKILL.md index 574f7b4f..b52cca38 100644 --- a/.github/skills/internal-tdd/SKILL.md +++ b/.github/skills/internal-tdd/SKILL.md @@ -58,6 +58,9 @@ mechanics. Those belong to the core skill. - For `mandatory`, load the core skill before changing behavior. - For `recommended`, keep the check at the most meaningful public or stable boundary. +- When adding tests, keep them under repository-root `tests/` and choose paths + that make the covered owner or checked behavior obvious. Use the nearest + owner for deeper layout conventions. - For `not suitable`, name the seam gap and the alternate validation path before implementation. - Tests or checks added after implementation are regression coverage only. Do diff --git a/AGENTS.md b/AGENTS.md index 42a3181e..52ba08b4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,8 +46,10 @@ This block is the portable source baseline used to generate - `AGENTS.md` owns stable repository-wide policy, precedence, tactical defaults, ownership boundaries, and routing anchors. -- Do not put operational procedures, checklists, file-shape recipes, command - playbooks, or tool-specific workflows here. +- Do not put long operational procedures, detailed checklists, detailed + file-shape recipes, command playbooks, or tool-specific workflows here. +- Short, globally safe best-practice defaults may live here when they improve + baseline behavior without turning this file into a procedure manual. - `tmp/` is temporary support only. Treat its contents as disposable working artifacts and do not commit files from `tmp/`. @@ -84,6 +86,9 @@ This block is the portable source baseline used to generate - Executable or evaluable behavior changes must use a test-first red-green-refactor loop: define the failing check, make the smallest implementation edit, then rerun the focused check and closest validation. +- Place tests under repository-root `tests/` using paths that make the owning + source or checked behavior obvious. Keep deeper layout conventions in the + nearest owner. - The failing check must exist before the first implementation edit unless a pre-code testability exception names the gap and alternate validation path. - Tests added after implementation are regression coverage only; they must not diff --git a/INTERNAL_CONTRACT.md b/INTERNAL_CONTRACT.md index f244f2cd..363e2845 100644 --- a/INTERNAL_CONTRACT.md +++ b/INTERNAL_CONTRACT.md @@ -206,10 +206,10 @@ Treat the current skill-first architecture as the source of truth. Do not infer - Python tests that target nested repository-owned source paths - Expected behavior: - Python tests remain under repository-root `tests/` - - when a test targets a nested source path, the test path under `tests/` mirrors the owned source directory closely enough that the tested folder is obvious - - when the tested source path starts with a hidden repository segment such as `.github/`, the mirrored test path uses a discoverable directory such as `tests/github/...` instead of a hidden path that pytest may skip - - flat root-level files such as `tests/test_*.py` are reserved for root-owned contracts or sources that also live at the repository root - - tests must not hide skill-owned or script-owned coverage behind unrelated flat filenames when a mirrored path would make the owner clear + - test paths under `tests/` should make the covered owner or checked behavior obvious + - when a test targets a nested source path, prefer a directory layout that stays discoverable to the active test runner and preserves owner clarity + - flat root-level files such as `tests/test_*.py` are best reserved for root-owned contracts or sources that also live at the repository root + - deeper layout conventions may be defined by a nearer language or tool owner when the repository-wide rule is not enough ### Naming And Operating Model diff --git a/tests/test_repository_test_layout_contract.py b/tests/test_repository_test_layout_contract.py index e16503d6..d4afdebb 100644 --- a/tests/test_repository_test_layout_contract.py +++ b/tests/test_repository_test_layout_contract.py @@ -8,6 +8,8 @@ if (parent / "AGENTS.md").exists() and (parent / ".github").exists() ) TESTS_ROOT = REPO_ROOT / "tests" +AGENTS = REPO_ROOT / "AGENTS.md" +INTERNAL_TDD = REPO_ROOT / ".github/skills/internal-tdd/SKILL.md" INTERNAL_CONTRACT = REPO_ROOT / "INTERNAL_CONTRACT.md" ANTI_PATTERNS = ( REPO_ROOT / ".github/skills/internal-review-code/references/anti-patterns-python.md" @@ -15,17 +17,20 @@ SKILL_PATH_PATTERN = re.compile(r"\.github/skills/([^/]+)/") -def test_contract_documents_discoverable_mirrored_test_paths() -> None: +def test_global_guidance_documents_generic_test_placement_rule() -> None: + agents_text = AGENTS.read_text(encoding="utf-8") + internal_tdd_text = INTERNAL_TDD.read_text(encoding="utf-8") contract_text = INTERNAL_CONTRACT.read_text(encoding="utf-8") anti_patterns_text = ANTI_PATTERNS.read_text(encoding="utf-8") - assert "testing-python-tests-mirror-source-layout" in contract_text - assert "tests/github/..." in contract_text - assert "flat root-level files such as `tests/test_*.py` are reserved" in contract_text - assert "discoverable mirrored source paths" in anti_patterns_text + assert "repository-root `tests/`" in agents_text + assert "make the owning\n source or checked behavior obvious" in agents_text + assert "When adding tests, keep them under repository-root `tests/`" in internal_tdd_text + assert "test paths under `tests/` should make the covered owner or checked behavior obvious" in contract_text + assert "without paths that make the covered owner or checked behavior obvious" in anti_patterns_text -def test_github_owned_python_tests_live_in_mirrored_paths() -> None: +def test_github_owned_python_tests_make_owner_obvious() -> None: violations: list[str] = [] for test_path in sorted(TESTS_ROOT.rglob("test_*.py")): @@ -36,9 +41,9 @@ def test_github_owned_python_tests_live_in_mirrored_paths() -> None: text = test_path.read_text(encoding="utf-8") if ".github/scripts/run.sh" in text: - if rel_path.parts[:2] != ("github", "scripts"): + if "github" not in rel_path.parts or "scripts" not in rel_path.parts: violations.append( - f"{rel_path} should live under tests/github/scripts/ for .github/scripts coverage" + f"{rel_path} should make the .github/scripts owner obvious" ) continue @@ -46,11 +51,10 @@ def test_github_owned_python_tests_live_in_mirrored_paths() -> None: if skill_match is None: continue - expected_prefix = ("github", "skills", skill_match.group(1)) - if rel_path.parts[:3] != expected_prefix: - expected = "/".join(expected_prefix) + skill_name = skill_match.group(1) + if "github" not in rel_path.parts or "skills" not in rel_path.parts or skill_name not in rel_path.parts: violations.append( - f"{rel_path} should live under tests/{expected}/ for .github/skills/{skill_match.group(1)}/ coverage" + f"{rel_path} should make the .github/skills/{skill_name}/ owner obvious" ) assert not violations, "\n".join(violations) From e2fa40b3c24338b47fb9fe6cc59541b51a8b74c8 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Wed, 8 Jul 2026 13:09:33 +0200 Subject: [PATCH 20/59] feat: add canonical chat report templates for problem and completion reporting in SKILL.md and sync-contract.md --- .../SKILL.md | 39 +++++++++++++++++ .../agents/openai.yaml | 2 +- .../references/sync-contract.md | 42 +++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/.github/skills/local-agent-sync-install-ai-resources/SKILL.md b/.github/skills/local-agent-sync-install-ai-resources/SKILL.md index ea69fd24..2b302739 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/SKILL.md +++ b/.github/skills/local-agent-sync-install-ai-resources/SKILL.md @@ -133,6 +133,45 @@ Then follow the exact text layout in `references/sync-contract.md`: Never report blocker codes alone. Translate each code into a plain-language reason and required follow-up. Never say a resource will change without stating what evidence selected the winner or triggered the recommendation. Bounded chat reports may omit excess change rows, but they must keep all blocker and attention rows visible and point to `--format json` for full detail. +### Canonical Chat Report Template + +When answering the user in chat, use this structure so the report stays easy to scan and easy to answer: + +1. `Status` + - One short line with the overall result. + - Prefer `completata`, `in corso`, `bloccata`, or `no-op`. +2. `What is happening` + - One short paragraph that explains the current state in plain language. +3. `Differences` + - List only the actionable changes. + - Number the items when the user may need to choose between them. + - Summarize unchanged or skipped resources by count unless the user asked for the full list. +4. `Why it stops` + - Explain the smallest real blocker, not the internal code name. + - Include only the drift or policy item that prevents the next step. +5. `Choices` + - Present only the actions the user can actually take now. + - Use numbered options. + - Put the recommended choice first. + - Keep each option to one line when possible. + +### Canonical Completion Template + +When the work is finished, close with this structure instead of repeating the full diff: + +1. `Result` + - Say whether the run completed, applied changes, or ended as no-op. +2. `What changed` + - One short paragraph with the concrete outcome. +3. `Final verification` + - State the strongest evidence available, such as a clean plan, hash match, or zero residual drift. +4. `Residuals` + - Include only if something still needs attention. + - If nothing remains, say `none`. +5. `Next step` + - If the run is done, say there is nothing else to do. + - If the run is blocked, name the single next action. + ## Bundled Automation - Prefer `.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh` for deterministic `plan`, `audit`, `doctor`, `apply`, and `bisync plan|apply` behavior. diff --git a/.github/skills/local-agent-sync-install-ai-resources/agents/openai.yaml b/.github/skills/local-agent-sync-install-ai-resources/agents/openai.yaml index 4abb450b..d0439e4b 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/agents/openai.yaml +++ b/.github/skills/local-agent-sync-install-ai-resources/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Home AI Resource Sync" short_description: "Plan, apply, audit, doctor, or bisync local AI home sync" - default_prompt: "Use $local-agent-sync-install-ai-resources to run safe repo-to-home sync with compact output by default; require explicit approval for apply or bisync apply, and summarize compact results in chat Markdown for user decisions." + default_prompt: "Use $local-agent-sync-install-ai-resources to run safe repo-to-home sync with compact output by default; require explicit approval for apply or bisync apply; when replying in chat, follow the canonical Problem Report or Completion Report template from the skill, keep differences and blockers plain-language, and render user choices as numbered options with the recommended choice first." diff --git a/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md b/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md index 2e1baea3..f86d6851 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md +++ b/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md @@ -94,6 +94,48 @@ Text reports must use a summary-first layout rather than a raw field dump. Use t Human-readable report tables should stay bounded for routine change and completed-action rows. Keep all blocker, attention, and readiness-failure rows visible. When rows are omitted, add an explicit omitted-count row and point to `--format json` for full detail. +## Canonical Chat Template + +Use this template when you need to answer the user directly in chat. Keep the wording plain and convert the labels into the conversation language when appropriate. + +### Problem Report + +Use this structure when the sync is not yet finished or when the user needs to choose a direction: + +1. `Status` + - One short line with the overall result. +2. `What is happening` + - One short paragraph that explains the current state in plain language. +3. `Differences` + - List only the actionable changes. + - Number the items when the user may need to choose between them. + - Summarize unchanged or skipped resources by count unless the user asked for the full list. +4. `Why it stops` + - Explain the smallest real blocker, not the internal code name. + - Include only the drift or policy item that prevents the next step. +5. `Choices` + - Present only the actions the user can actually take now. + - Use numbered options. + - Put the recommended choice first. + - Keep each option to one line when possible. + +### Completion Report + +Use this structure when the work is finished: + +1. `Result` + - Say whether the run completed, applied changes, or ended as no-op. +2. `What changed` + - One short paragraph with the concrete outcome. +3. `Final verification` + - State the strongest evidence available, such as a clean plan, hash match, or zero residual drift. +4. `Residuals` + - Include only if something still needs attention. + - If nothing remains, say `none`. +5. `Next step` + - If the run is done, say there is nothing else to do. + - If the run is blocked, name the single next action. + ### Shared Header Always start with a short status line that includes: From d18e8d40325e7b2bd5dd5b47a2a66840f38e32e9 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Wed, 8 Jul 2026 13:16:17 +0200 Subject: [PATCH 21/59] feat: enhance sync and install report formatting with emoji headings and conditional section rendering --- .../references/sync-contract.md | 44 ++--- .../scripts/sync_output.py | 161 ++++++++++-------- .../scripts/test_contracts.py | 102 +++++++++++ 3 files changed, 214 insertions(+), 93 deletions(-) diff --git a/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md b/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md index f86d6851..0fab99a9 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md +++ b/.github/skills/local-agent-sync-install-ai-resources/references/sync-contract.md @@ -100,20 +100,21 @@ Use this template when you need to answer the user directly in chat. Keep the wo ### Problem Report -Use this structure when the sync is not yet finished or when the user needs to choose a direction: +Use this structure when the sync is not yet finished or when the user needs to choose a direction. Skip any section that would be empty instead of rendering a heading with no content: -1. `Status` +1. `🚦 Status` - One short line with the overall result. -2. `What is happening` +2. `📌 What is happening` - One short paragraph that explains the current state in plain language. -3. `Differences` +3. `🔁 Differences` - List only the actionable changes. + - Omit the section entirely when there is nothing actionable to show. - Number the items when the user may need to choose between them. - Summarize unchanged or skipped resources by count unless the user asked for the full list. -4. `Why it stops` +4. `⛔ Why it stops` - Explain the smallest real blocker, not the internal code name. - Include only the drift or policy item that prevents the next step. -5. `Choices` +5. `🎯 Choices` - Present only the actions the user can actually take now. - Use numbered options. - Put the recommended choice first. @@ -121,24 +122,25 @@ Use this structure when the sync is not yet finished or when the user needs to c ### Completion Report -Use this structure when the work is finished: +Use this structure when the work is finished. Skip any section that would be empty instead of rendering a heading with no content: -1. `Result` +1. `✅ Result` - Say whether the run completed, applied changes, or ended as no-op. -2. `What changed` +2. `🛠️ What changed` - One short paragraph with the concrete outcome. -3. `Final verification` + - Omit the section entirely when nothing changed. +3. `🔎 Final verification` - State the strongest evidence available, such as a clean plan, hash match, or zero residual drift. -4. `Residuals` +4. `🧾 Residuals` - Include only if something still needs attention. - If nothing remains, say `none`. -5. `Next step` +5. `➡️ Next step` - If the run is done, say there is nothing else to do. - If the run is blocked, name the single next action. ### Shared Header -Always start with a short status line that includes: +Always start with a short `🚦 Status` line that includes: - mode - human-friendly lane label, such as `repo-to-home install` for install or `repo-home drift` for bisync @@ -151,13 +153,13 @@ Always start with a short status line that includes: Use these stable sections when rendering model-facing reports: -- `Summary`: target, resource, drift, blocked, and already-aligned counts. -- `Readiness`: doctor-only non-ok checks with why they matter, what blocks next, and the recommended action. -- `Changes`: proposed or completed writes, with one row per changed resource. -- `Attention`: blockers, ambiguous drift, or decisions that need a user. -- `Validation`: strongest available evidence such as hash match, manifest path, state path, or post-apply clean plan. -- `Remaining Work`: include only when something remains. -- `Next`: structured next action and reason. +- `🧭 Summary`: target, resource, drift, blocked, and already-aligned counts. +- `🩺 Readiness`: doctor-only non-ok checks with why they matter, what blocks next, and the recommended action. +- `🛠️ Changes`: proposed or completed writes, with one row per changed resource. +- `⚠️ Attention`: blockers, ambiguous drift, or decisions that need a user. +- `🔎 Validation`: strongest available evidence such as hash match, manifest path, state path, or post-apply clean plan. +- `🧾 Remaining Work`: include only when something remains. +- `➡️ Next`: structured next action and reason. Do not list every unchanged managed resource in model-facing reports. Summarize skipped or already-aligned resources by count unless the user asks for full audit detail or selects JSON output. @@ -172,7 +174,7 @@ Use this table for missing roots, documentation gaps, manifest problems, permiss ### Sync, Plan, Audit, And Bisync Plan Report -For top-level `sync`, use this compact chat order: `Status`, `Summary`, `Auto-applied` or `Planned repo-to-home copies`, `Stopped on`, `Validation`, and `Next`. +For top-level `sync`, use this compact chat order: `🚦 Status`, `🧭 Summary`, `🚀 Auto-applied` or `📋 Planned repo-to-home copies`, `⛔ Stopped on`, `🔎 Validation`, and `➡️ Next`. After the shared header and summary, show one change-oriented table and one attention table when they are useful. diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_output.py b/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_output.py index 5c310831..f3b83254 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_output.py +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_output.py @@ -37,6 +37,19 @@ } REPORT_TABLE_ROW_LIMIT = 8 +REPORT_SECTION_EMOJIS: dict[str, str] = { + "Summary": "🧭", + "Auto-applied": "🚀", + "Planned repo-to-home copies": "📋", + "Stopped on": "⛔", + "Changes": "🛠️", + "Completed": "✅", + "Attention": "⚠️", + "Validation": "🔎", + "Readiness": "🩺", + "Remaining Work": "🧾", + "Next": "➡️", +} def dump_compact_json(payload: dict[str, object]) -> str: @@ -123,7 +136,7 @@ def render_sync_report(payload: dict[str, object]) -> str: lines: list[str] = [] lines.append( - f"Status: sync | status={status} | targets={targets} | next_action={next_action} | reason={reason}" + f"🚦 Status: sync | status={status} | targets={targets} | next_action={next_action} | reason={reason}" ) lines.append("") @@ -149,35 +162,37 @@ def render_sync_report(payload: dict[str, object]) -> str: auto_applied_section, auto_applied_reason_column, auto_applied_omitted_label, - auto_applied_none_reason, + _auto_applied_none_reason, ) = _sync_apply_section_labels( install_payload if isinstance(install_payload, dict) else {} ) - lines.extend(_report_section(auto_applied_section)) - lines.extend( - _table_lines( - ["Skill or path", auto_applied_reason_column, "Verification"], - _bounded_rows( - _sync_auto_applied_rows(install_payload if isinstance(install_payload, dict) else {}), - auto_applied_omitted_label, - ), - none_row=["none", auto_applied_none_reason, "none"], - ) + auto_applied_rows = _bounded_rows( + _sync_auto_applied_rows(install_payload if isinstance(install_payload, dict) else {}), + auto_applied_omitted_label, ) - lines.append("") - - lines.extend(_report_section("Stopped on")) - lines.extend( - _table_lines( - ["Skill or path", "Direction", "Differences or reason"], - _sync_stopped_rows( - install_payload if isinstance(install_payload, dict) else {}, - bisync_payload if isinstance(bisync_payload, dict) else {}, - ), - none_row=["none", "none", "No home-owned or ambiguous drift stopped sync."], + if auto_applied_rows: + lines.extend(_report_section(auto_applied_section)) + lines.extend( + _table_lines( + ["Skill or path", auto_applied_reason_column, "Verification"], + auto_applied_rows, + ) ) + lines.append("") + + stopped_rows = _sync_stopped_rows( + install_payload if isinstance(install_payload, dict) else {}, + bisync_payload if isinstance(bisync_payload, dict) else {}, ) - lines.append("") + if stopped_rows: + lines.extend(_report_section("Stopped on")) + lines.extend( + _table_lines( + ["Skill or path", "Direction", "Differences or reason"], + stopped_rows, + ) + ) + lines.append("") lines.extend(_report_section("Validation")) lines.extend( @@ -484,7 +499,7 @@ def render_install_report(payload: dict[str, object]) -> str: lines: list[str] = [] lines.append( - f"Status: {lane_label} | mode={mode} | targets={_join_or_none(targets)} | status={status} | blockers={len(blocked_codes)} | next_action={next_action}" + f"🚦 Status: {lane_label} | mode={mode} | targets={_join_or_none(targets)} | status={status} | blockers={len(blocked_codes)} | next_action={next_action}" ) lines.append("") @@ -504,19 +519,18 @@ def render_install_report(payload: dict[str, object]) -> str: lines.append("") planned_rows = _bounded_rows(_install_planned_rows(payload), "change") - lines.extend(_report_section("Changes")) - lines.extend( - _table_lines( - ["Resource or path", "Lane", "Planned action", "Why this will change", "Evidence or winner"], - planned_rows, - none_row=["none", "install", "no-op", "No planned changes.", "none"], + if planned_rows: + lines.extend(_report_section("Changes")) + lines.extend( + _table_lines( + ["Resource or path", "Lane", "Planned action", "Why this will change", "Evidence or winner"], + planned_rows, + ) ) - ) - lines.append("") + lines.append("") completed_rows = _bounded_rows(_install_completed_rows(payload), "completed action") if completed_rows: - lines.append("") lines.extend(_report_section("Completed")) lines.extend( _table_lines( @@ -524,18 +538,18 @@ def render_install_report(payload: dict[str, object]) -> str: completed_rows, ) ) - lines.append("") + lines.append("") blocker_rows = _install_blocker_rows(payload) - lines.extend(_report_section("Attention")) - lines.extend( - _table_lines( - ["Code or status", "Resource or path", "Why it needs attention", "Required user action"], - blocker_rows, - none_row=["none", "none", "No blockers need attention.", "none"], + if blocker_rows: + lines.extend(_report_section("Attention")) + lines.extend( + _table_lines( + ["Code or status", "Resource or path", "Why it needs attention", "Required user action"], + blocker_rows, + ) ) - ) - lines.append("") + lines.append("") lines.extend(_report_section("Validation")) validation_rows = [["Validation status", status]] @@ -574,7 +588,7 @@ def render_doctor_report(payload: dict[str, object]) -> str: lines: list[str] = [] lines.append( - f"Status: {lane_label} | mode=doctor | targets={_join_or_none(targets)} | status={status} | blockers={len(blocked_codes)} | next_action={next_action}" + f"🚦 Status: {lane_label} | mode=doctor | targets={_join_or_none(targets)} | status={status} | blockers={len(blocked_codes)} | next_action={next_action}" ) lines.append("") @@ -592,15 +606,16 @@ def render_doctor_report(payload: dict[str, object]) -> str: ) lines.append("") - lines.extend(_report_section("Readiness")) - lines.extend( - _table_lines( - ["Check or path", "Status", "Why it matters", "What blocks next", "Recommended action"], - _doctor_readiness_rows(payload), - none_row=["all checks", "ok", "No readiness blockers found.", "none", "Run plan or sync when ready."], + readiness_rows = _doctor_readiness_rows(payload) + if readiness_rows: + lines.extend(_report_section("Readiness")) + lines.extend( + _table_lines( + ["Check or path", "Status", "Why it matters", "What blocks next", "Recommended action"], + readiness_rows, + ) ) - ) - lines.append("") + lines.append("") lines.extend(_report_section("Validation")) validation_rows = [["Validation status", status]] @@ -629,7 +644,7 @@ def render_bisync_report(payload: dict[str, object]) -> str: lines: list[str] = [] lines.append( - f"Status: {lane_label} | mode={mode} | target=skills | status={status} | drift_total={drift_total} | blockers={len(blocked_codes)} | next_action={next_action}" + f"🚦 Status: {lane_label} | mode={mode} | target=skills | status={status} | drift_total={drift_total} | blockers={len(blocked_codes)} | next_action={next_action}" ) lines.append("") @@ -649,18 +664,18 @@ def render_bisync_report(payload: dict[str, object]) -> str: ) lines.append("") - lines.extend(_report_section("Changes")) - lines.extend( - _table_lines( - ["Resource or path", "Lane", "Planned action", "Why this will change", "Evidence or winner"], - _bounded_rows(_bisync_planned_rows(payload), "change"), - none_row=["none", "bisync", "no-op", "No drift detected.", "none"], + planned_rows = _bounded_rows(_bisync_planned_rows(payload), "change") + if planned_rows: + lines.extend(_report_section("Changes")) + lines.extend( + _table_lines( + ["Resource or path", "Lane", "Planned action", "Why this will change", "Evidence or winner"], + planned_rows, + ) ) - ) completed_rows = _bounded_rows(_bisync_completed_rows(payload), "completed action") if completed_rows: - lines.append("") lines.extend(_report_section("Completed")) lines.extend( _table_lines( @@ -668,17 +683,18 @@ def render_bisync_report(payload: dict[str, object]) -> str: completed_rows, ) ) - lines.append("") + lines.append("") - lines.extend(_report_section("Attention")) - lines.extend( - _table_lines( - ["Code or status", "Resource or path", "Why it needs attention", "Required user action"], - _bisync_blocker_rows(payload), - none_row=["none", "none", "No blockers need attention.", "none"], + blocker_rows = _bisync_blocker_rows(payload) + if blocker_rows: + lines.extend(_report_section("Attention")) + lines.extend( + _table_lines( + ["Code or status", "Resource or path", "Why it needs attention", "Required user action"], + blocker_rows, + ) ) - ) - lines.append("") + lines.append("") lines.extend(_report_section("Validation")) verification_rows = [["Verification status", status]] @@ -694,7 +710,6 @@ def render_bisync_report(payload: dict[str, object]) -> str: remaining_rows = _remaining_work_rows(payload) if remaining_rows: - lines.append("") lines.extend(_report_section("Remaining Work")) lines.extend( _table_lines( @@ -710,7 +725,9 @@ def render_bisync_report(payload: dict[str, object]) -> str: def _report_section(name: str) -> list[str]: - return [f"## {name}"] + emoji = REPORT_SECTION_EMOJIS.get(name) + label = f"{emoji} {name}" if emoji else name + return [f"## {label}"] def _bullet_lines(items: list[str]) -> list[str]: diff --git a/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_contracts.py b/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_contracts.py index 92367f7b..660ea17d 100644 --- a/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_contracts.py +++ b/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_contracts.py @@ -36,6 +36,11 @@ parse_targets, state_root_for_home, ) +from sync_output import ( # noqa: E402 + render_doctor_report, + render_install_report, + render_sync_report, +) from sync_home_ai_resources import ( # noqa: E402 bisync_requires_review, install_auto_apply_blockers, @@ -128,6 +133,103 @@ def test_parse_targets_orders_cross_aliases_and_rejects_unknown() -> None: parse_targets("skills,invalid") +def test_render_install_report_omits_empty_sections_and_uses_emoji_headings() -> None: + report = render_install_report( + { + "mode": "plan", + "selected_targets": ["skills"], + "status": "ok", + "validation": "ok", + "blocked_codes": [], + "operations": [], + "source_resources_considered": 0, + "state_path": "/tmp/state", + "next_action": { + "action": "done", + "allowed": True, + "requires_explicit_approval": False, + "command": "", + "reason": "No work.", + }, + } + ) + + assert "🚦 Status:" in report + assert "## 🧭 Summary" in report + assert "## 🛠️ Changes" not in report + assert "## ✅ Completed" not in report + assert "## ⚠️ Attention" not in report + assert "## 🔎 Validation" in report + assert "## ➡️ Next" in report + + +def test_render_doctor_report_omits_readiness_when_everything_is_ok() -> None: + report = render_doctor_report( + { + "selected_targets": ["skills"], + "status": "ok", + "validation": "ok", + "checks": [ + { + "name": "runtime root", + "path": "/tmp/home/.agents/skills", + "status": "ok", + } + ], + "state_path": "/tmp/state", + "next_action": { + "action": "done", + "allowed": True, + "requires_explicit_approval": False, + "command": "", + "reason": "No work.", + }, + } + ) + + assert "🚦 Status:" in report + assert "## 🧭 Summary" in report + assert "## 🩺 Readiness" not in report + assert "## 🔎 Validation" in report + assert "## ➡️ Next" in report + + +def test_render_sync_report_omits_empty_action_sections() -> None: + report = render_sync_report( + { + "status": "done", + "reason": "No work.", + "install": { + "selected_targets": ["skills"], + "operations": [], + "validation": "ok", + "state_path": "/tmp/state", + "manifest_path": "/tmp/manifest", + }, + "bisync": { + "mode": "plan", + "drifts": [], + "verification": {"status": "ok"}, + }, + "next_action": { + "action": "done", + "allowed": True, + "requires_explicit_approval": False, + "command": "", + "reason": "No work.", + }, + } + ) + + assert "🚦 Status:" in report + assert "## 🧭 Summary" in report + assert "## 🚀 Auto-applied" not in report + assert "## 📋 Planned repo-to-home copies" not in report + assert "## ⛔ Stopped on" not in report + assert "## 🔎 Validation" in report + assert "## ➡️ Next" in report + + def test_build_bisync_plan_filters_local_and_excluded_bundles(tmp_path: Path) -> None: refs_dir = ( tmp_path From 497ec342f9f72c6084fd07663c7d2a493c4688a5 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Wed, 8 Jul 2026 13:32:33 +0200 Subject: [PATCH 22/59] feat: enhance chat report templates with emoji-led headings for improved clarity --- .../SKILL.md | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/skills/local-agent-sync-install-ai-resources/SKILL.md b/.github/skills/local-agent-sync-install-ai-resources/SKILL.md index 2b302739..d5dfc895 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/SKILL.md +++ b/.github/skills/local-agent-sync-install-ai-resources/SKILL.md @@ -12,7 +12,8 @@ description: Use when planning, auditing, or applying allowlisted home-directory Use this skill as the operating engine for `.github/agents/local-sync-install-ai-resources.agent.md`. The paired agent is only a thin UX wrapper; this skill owns mode selection, approval posture, safety gates, and report interpretation for repo-to-home sync -and bisync. Keep user-visible output deterministic, bounded, and summary-first. +and bisync. Keep user-visible output deterministic, bounded, summary-first, +and emoji-led in chat. Canonical command examples use `.github/skills/local-agent-sync-install-ai-resources/scripts/run.sh`. The CLI defaults to `--format compact` for AI/tool iteration. Use @@ -135,21 +136,21 @@ Never report blocker codes alone. Translate each code into a plain-language reas ### Canonical Chat Report Template -When answering the user in chat, use this structure so the report stays easy to scan and easy to answer: +When answering the user in chat, use this structure so the report stays easy to scan and easy to answer. Use emoji-led headings in chat: -1. `Status` +1. `🚦 Status` - One short line with the overall result. - Prefer `completata`, `in corso`, `bloccata`, or `no-op`. -2. `What is happening` +2. `📌 What is happening` - One short paragraph that explains the current state in plain language. -3. `Differences` +3. `🔁 Differences` - List only the actionable changes. - Number the items when the user may need to choose between them. - Summarize unchanged or skipped resources by count unless the user asked for the full list. -4. `Why it stops` +4. `⛔ Why it stops` - Explain the smallest real blocker, not the internal code name. - Include only the drift or policy item that prevents the next step. -5. `Choices` +5. `🎯 Choices` - Present only the actions the user can actually take now. - Use numbered options. - Put the recommended choice first. @@ -157,18 +158,18 @@ When answering the user in chat, use this structure so the report stays easy to ### Canonical Completion Template -When the work is finished, close with this structure instead of repeating the full diff: +When the work is finished, close with this structure instead of repeating the full diff. Use emoji-led headings in chat: -1. `Result` +1. `✅ Result` - Say whether the run completed, applied changes, or ended as no-op. -2. `What changed` +2. `🛠️ What changed` - One short paragraph with the concrete outcome. -3. `Final verification` +3. `🔎 Final verification` - State the strongest evidence available, such as a clean plan, hash match, or zero residual drift. -4. `Residuals` +4. `🧾 Residuals` - Include only if something still needs attention. - If nothing remains, say `none`. -5. `Next step` +5. `➡️ Next step` - If the run is done, say there is nothing else to do. - If the run is blocked, name the single next action. From e77e9a4acc8c4b99bfdd86eabe8ba1a9ec60e82c Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Wed, 8 Jul 2026 13:46:47 +0200 Subject: [PATCH 23/59] feat: rename and replace `internal-gateway-idea-brainstorming` with `internal-gateway-idea` across documentation and scripts for consistency --- .github/CHANGELOG.md | 4 ++-- .github/INVENTORY.md | 2 +- .github/README.md | 2 +- .github/agents/README.md | 4 ++-- ...ing.agent.md => internal-gateway-idea.agent.md} | 6 +++--- .../agents/local-sync-external-resources.agent.md | 2 +- ...-sync-global-copilot-configs-into-repo.agent.md | 2 +- .github/scripts/benchmark_skill_tokens.py | 7 +++---- .github/scripts/lib/token_risks.py | 2 +- .../references/example-transformations.md | 2 +- .../references/subagent-patterns.md | 2 +- .../SKILL.md | 14 +++++++------- .../internal-agent-support-next-step/SKILL.md | 4 ++-- .github/skills/internal-gateway-idea/SKILL.md | 6 +++--- .../internal-gateway-idea/agents/openai.yaml | 4 ++-- .../internal-gateway-idea/references/workflow.md | 12 +++--------- .../scripts/audit_workflow.py | 9 +++++---- .../references/external-watchlist.yaml | 2 +- .../references/home-sync-catalog.yaml | 4 ++-- 19 files changed, 42 insertions(+), 48 deletions(-) rename .github/agents/{internal-gateway-idea-brainstorming.agent.md => internal-gateway-idea.agent.md} (90%) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 346c4f0a..c14ff587 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -21,11 +21,11 @@ Use this format for new updates: ## 2026-06-02 -- Added `internal-gateway-idea-brainstorming` as a visible fourth gateway skill and thin Copilot wrapper agent, owning substantive idea definition, guided decision interview, convergence, Definition Brief, mandatory critical pass, and validated handoff before operational planning. +- Added `internal-gateway-idea` as a visible fourth gateway skill and thin Copilot wrapper agent, owning substantive idea definition, guided decision interview, convergence, Definition Brief, mandatory critical pass, and validated handoff before operational planning. - Absorbed and retired `internal-idea-define-advisor`, moving its useful owner-map and question-bank concepts into the new gateway bundle without a compatibility stub or alias. - Narrowed `internal-gateway-operational-flow` to delegate unresolved idea work visibly to the new gateway and consume validated Definition Brief handoffs for `plan` without repeating ideation or its critical pass. - Added guided decision interview semantics: evidence-first discovery, iterative numbered question blocks through `grill-me`, visible defaults, compact decision ledger, proportional depth, and checkpoint states. -- Retired the standalone `idea-refine` skill immediately after extracting its useful shaping frameworks and evaluation criteria into `internal-gateway-idea-brainstorming` references, preserving its upstream license and removing the competing runtime entrypoint. +- Retired the standalone `idea-refine` skill immediately after extracting its useful shaping frameworks and evaluation criteria into `internal-gateway-idea` references, preserving its upstream license and removing the competing runtime entrypoint. - Updated adjacent routing owners (`internal-agent-support-next-step`, `internal-agent-support-lane-change-engine`, `internal-gateway-simple-task`, `internal-gateway-critical-master`) to recognize the new gateway in handoffs and lane-change recommendations. - Updated `.github/agents/README.md`, `.github/README.md`, `INTERNAL_CONTRACT.md`, `docs`, wrapper-alignment, mode-contracts, subagent-patterns, home-sync catalog, token benchmarks, and regression tests for the four-gateway model. diff --git a/.github/INVENTORY.md b/.github/INVENTORY.md index 264c584f..3e4cbc7a 100644 --- a/.github/INVENTORY.md +++ b/.github/INVENTORY.md @@ -178,7 +178,7 @@ These imported `openai-*` office skills remain support-only depth for repositori - `.github/agents/internal-review-code.agent.md` - `.github/agents/internal-gateway-critical-master.agent.md` -- `.github/agents/internal-gateway-idea-brainstorming.agent.md` +- `.github/agents/internal-gateway-idea.agent.md` - `.github/agents/internal-gateway-review.agent.md` - `.github/agents/internal-gateway-simple-task.agent.md` - `.github/agents/local-sync-external-resources.agent.md` diff --git a/.github/README.md b/.github/README.md index ad748f65..b895198b 100644 --- a/.github/README.md +++ b/.github/README.md @@ -10,7 +10,7 @@ customization assets maintained in `cloud-strategy.github`. ## Agents - Canonical repository-owned gateway agents: - `internal-gateway-idea-brainstorming`, `internal-gateway-review`, + `internal-gateway-idea`, `internal-gateway-review`, `internal-gateway-critical-master`, `internal-gateway-simple-task` - Specialist repository-owned review agents: `internal-review-code` for code-focused review before merge or follow-up action. diff --git a/.github/agents/README.md b/.github/agents/README.md index efb29443..b8aff5f6 100644 --- a/.github/agents/README.md +++ b/.github/agents/README.md @@ -5,7 +5,7 @@ repo-only sync workflows. ## Agent-Owned Core -- `internal-gateway-idea-brainstorming`: owns substantive idea definition, +- `internal-gateway-idea`: owns substantive idea definition, critical challenge, and retained planning before execution. - `internal-gateway-review`: owns generic defect-first review for non-code and mixed artifacts before fixes. @@ -19,7 +19,7 @@ repo-only sync workflows. | Agent | Use when | | --- | --- | -| `internal-gateway-idea-brainstorming` | A vague idea or unresolved goal needs definition and retained planning. | +| `internal-gateway-idea` | A vague idea or unresolved goal needs definition and retained planning. | | `internal-gateway-review` | A concrete non-code or mixed artifact needs defect-first review before fixes. | | `internal-review-code` | A concrete code target needs dedicated code review before merge or follow-up action. | | `internal-gateway-critical-master` | A proposal or plan needs pressure before action. | diff --git a/.github/agents/internal-gateway-idea-brainstorming.agent.md b/.github/agents/internal-gateway-idea.agent.md similarity index 90% rename from .github/agents/internal-gateway-idea-brainstorming.agent.md rename to .github/agents/internal-gateway-idea.agent.md index b0af104f..8c4cc341 100644 --- a/.github/agents/internal-gateway-idea-brainstorming.agent.md +++ b/.github/agents/internal-gateway-idea.agent.md @@ -1,5 +1,5 @@ --- -name: internal-gateway-idea-brainstorming +name: internal-gateway-idea description: "Use this agent when a repository-owned request starts with a vague idea, unclear goal, unresolved option set, or needs substantive definition, convergence, critical challenge, and retained planning before execution." tools: ["read", "edit", "search", "execute", "web"] disable-model-invocation: true @@ -19,8 +19,8 @@ handoffs: send: false --- -# Internal Gateway Idea Brainstorming +# Internal Gateway Idea ## Core Skill -- `internal-gateway-idea-brainstorming` +- `internal-gateway-idea` diff --git a/.github/agents/local-sync-external-resources.agent.md b/.github/agents/local-sync-external-resources.agent.md index 6b207aab..2aa0882e 100644 --- a/.github/agents/local-sync-external-resources.agent.md +++ b/.github/agents/local-sync-external-resources.agent.md @@ -32,7 +32,7 @@ retire, and anti-drift procedure in the paired core skill. audit, plan, or dry run. - Treat `apply` as invalid until `internal-copilot-audit` has completed preflight and no unresolved `blocking` findings remain. -- Start with `internal-gateway-idea-brainstorming` when the catalog problem still +- Start with `internal-gateway-idea` when the catalog problem still needs option framing, staged planning, or a user-supplied multi-step remediation plan. - Use `internal-agent-creator` when the task is one concrete agent contract, diff --git a/.github/agents/local-sync-global-copilot-configs-into-repo.agent.md b/.github/agents/local-sync-global-copilot-configs-into-repo.agent.md index ffd2f44e..8f6cfb02 100644 --- a/.github/agents/local-sync-global-copilot-configs-into-repo.agent.md +++ b/.github/agents/local-sync-global-copilot-configs-into-repo.agent.md @@ -23,7 +23,7 @@ Use this agent for route selection, mode selection, approval posture, and bounda - Use this agent for consumer-repository baseline propagation, drift assessment, `plan`, `audit`, and explicit `apply` runs. - Use this agent when the target repository must inherit the root `AGENTS.md` policy model, review-only Copilot configuration, `.github/INVENTORY.md`, repository-root `LESSONS_LEARNED.md`, and explicitly shared hygiene files. - Select `apply` only on explicit request, after the current evidence shows a conflict-safe plan and no unmanaged target-local cleanup is being implied. -- Do not use this agent for source-side catalog governance, external-resource refreshes, or managed-scope redesign in this repository; recommend `local-sync-external-resources` or `internal-gateway-idea-brainstorming` as appropriate. +- Do not use this agent for source-side catalog governance, external-resource refreshes, or managed-scope redesign in this repository; recommend `local-sync-external-resources` or `internal-gateway-idea` as appropriate. - Do not use this agent for one-resource agent or skill authoring; recommend `internal-agent-creator` or `internal-skill-creator` as appropriate. - When current platform behavior decides sync policy, validate it through `internal-copilot-docs-research` before changing the contract. diff --git a/.github/scripts/benchmark_skill_tokens.py b/.github/scripts/benchmark_skill_tokens.py index 2bf7ca6d..cc203524 100644 --- a/.github/scripts/benchmark_skill_tokens.py +++ b/.github/scripts/benchmark_skill_tokens.py @@ -107,15 +107,14 @@ def build_scenario_report(root: Path) -> list[dict[str, Any]]: return reports -GATEWAY_SKILL = "internal-gateway-idea-brainstorming" +GATEWAY_SKILL = "internal-gateway-idea" REVIEW_COUNTERCHECK_SKILL = "internal-gateway-critical-master" GATEWAY_REQUIRED_CONTEXT_SCENARIOS: dict[str, list[str]] = { "Direct execute": [GATEWAY_SKILL], "Define Gate 0": [GATEWAY_SKILL, "grill-me"], "Define idea and critical": [ - GATEWAY_SKILL, "grill-me", "internal-gateway-idea-brainstorming", - "internal-gateway-critical-master", + GATEWAY_SKILL, "grill-me", "internal-gateway-critical-master", ], "Plan handoff": [GATEWAY_SKILL, "internal-gateway-writing-plans", "internal-agent-support-next-step"], "Approved apply-plan": [GATEWAY_SKILL, "internal-gateway-execute-plans"], @@ -124,7 +123,7 @@ def build_scenario_report(root: Path) -> list[dict[str, Any]]: ], } -IDEA_GATEWAY_SKILL = "internal-gateway-idea-brainstorming" +IDEA_GATEWAY_SKILL = "internal-gateway-idea" IDEA_GATEWAY_SCENARIOS: dict[str, list[str]] = { "Idea core entry": [IDEA_GATEWAY_SKILL], diff --git a/.github/scripts/lib/token_risks.py b/.github/scripts/lib/token_risks.py index c078885c..028c65ee 100644 --- a/.github/scripts/lib/token_risks.py +++ b/.github/scripts/lib/token_risks.py @@ -51,7 +51,7 @@ ) -GATEWAY_CORE_SKILL_PATH = ".github/skills/internal-gateway-idea-brainstorming/SKILL.md" +GATEWAY_CORE_SKILL_PATH = ".github/skills/internal-gateway-idea/SKILL.md" GATEWAY_CORE_BYTE_BUDGET = 16286 GATEWAY_REQUIRED_CONTEXT_BYTE_BUDGET = 30000 GATEWAY_UNIVERSAL_PRELOAD_MARKERS = ( diff --git a/.github/skills/internal-agent-creator/references/example-transformations.md b/.github/skills/internal-agent-creator/references/example-transformations.md index 0b4caa85..d5733c99 100644 --- a/.github/skills/internal-agent-creator/references/example-transformations.md +++ b/.github/skills/internal-agent-creator/references/example-transformations.md @@ -191,7 +191,7 @@ You are the provider-specific cloud command center for architecture tradeoffs, i - Clarify the critical requirements first: scale, resilience, compliance, budget, and operating constraints. - Confirm current provider facts with official documentation before finalizing recommendations. - State the main tradeoff explicitly across security, reliability, performance, cost, or operations. -- Do not use this agent when the question is still provider-agnostic or cross-boundary; prefer `internal-gateway-idea-brainstorming`. +- Do not use this agent when the question is still provider-agnostic or cross-boundary; prefer `internal-gateway-idea`. ## Output Expectations diff --git a/.github/skills/internal-agent-creator/references/subagent-patterns.md b/.github/skills/internal-agent-creator/references/subagent-patterns.md index 9d49c8eb..2d4d1cfe 100644 --- a/.github/skills/internal-agent-creator/references/subagent-patterns.md +++ b/.github/skills/internal-agent-creator/references/subagent-patterns.md @@ -73,7 +73,7 @@ Focused instructions for domain A work. ### Router with explicit dispatch targets ```yaml -agents: ['internal-gateway-idea-brainstorming', 'internal-gateway-review', 'internal-gateway-critical-master', 'internal-gateway-simple-task'] +agents: ['internal-gateway-idea', 'internal-gateway-review', 'internal-gateway-critical-master', 'internal-gateway-simple-task'] ``` Only these four agents can be invoked as subagents. The platform enforces this. diff --git a/.github/skills/internal-agent-support-lane-change-engine/SKILL.md b/.github/skills/internal-agent-support-lane-change-engine/SKILL.md index be69e715..e9eef9e7 100644 --- a/.github/skills/internal-agent-support-lane-change-engine/SKILL.md +++ b/.github/skills/internal-agent-support-lane-change-engine/SKILL.md @@ -9,7 +9,7 @@ description: Use when a repository-owned internal agent needs a consistent user- - `internal-gateway-review`: source lane when review findings leave a local fix, planning gap, or assumption challenge. - `internal-gateway-simple-task`: source or target lane for concrete local follow-up work. -- `internal-gateway-idea-brainstorming`: target lane when planning or scope definition is needed. +- `internal-gateway-idea`: target lane when planning or scope definition is needed. - `internal-gateway-critical-master`: target lane when assumption pressure-testing dominates. - `internal-gateway-execute-plans`: target lane when approved retained-plan execution is the next step. @@ -26,19 +26,19 @@ agents. - Stop before doing off-lane work. - Explain the concrete mismatch. - Recommend exactly one better owner when the next lane is clear. -- Fail safe to `internal-gateway-idea-brainstorming` when the next lane is still ambiguous. +- Fail safe to `internal-gateway-idea` when the next lane is still ambiguous. ## Recommendation Matrix | Current agent | When the boundary breaks | Recommend | | --- | --- | --- | | `internal-gateway-review` | Findings now leave only a concrete local fix | `internal-gateway-simple-task` | -| `internal-gateway-review` | Findings reopen planning or scope definition | `internal-gateway-idea-brainstorming` | +| `internal-gateway-review` | Findings reopen planning or scope definition | `internal-gateway-idea` | | `internal-gateway-review` | Assumption pressure-testing becomes dominant | `internal-gateway-critical-master` | -| `internal-gateway-simple-task` | Planning or governance becomes dominant | `internal-gateway-idea-brainstorming` | +| `internal-gateway-simple-task` | Planning or governance becomes dominant | `internal-gateway-idea` | | `internal-gateway-simple-task` | Defect-first analysis becomes dominant | `internal-gateway-review` | | `internal-gateway-simple-task` | Assumption pressure-testing becomes dominant | `internal-gateway-critical-master` | -| `internal-gateway-critical-master` | The next step is planning | `internal-gateway-idea-brainstorming` | +| `internal-gateway-critical-master` | The next step is planning | `internal-gateway-idea` | | `internal-gateway-critical-master` | The next step is evidence-first review | `internal-gateway-review` | -| `internal-gateway-idea-brainstorming` | A retained `compact` plan is approved for execution | `internal-gateway-simple-task` | -| `internal-gateway-idea-brainstorming` | A retained `extended` plan is approved for execution | `internal-gateway-execute-plans` | +| `internal-gateway-idea` | A retained `compact` plan is approved for execution | `internal-gateway-simple-task` | +| `internal-gateway-idea` | A retained `extended` plan is approved for execution | `internal-gateway-execute-plans` | diff --git a/.github/skills/internal-agent-support-next-step/SKILL.md b/.github/skills/internal-agent-support-next-step/SKILL.md index 64d446eb..538e0da8 100644 --- a/.github/skills/internal-agent-support-next-step/SKILL.md +++ b/.github/skills/internal-agent-support-next-step/SKILL.md @@ -7,7 +7,7 @@ description: Use when a repository-owned agent or prompt needs to package an alr ## Referenced skills -- `internal-gateway-idea-brainstorming`: source lane that may need a visible next-step transition. +- `internal-gateway-idea`: source lane that may need a visible next-step transition. - `internal-gateway-review`: source lane that may need a visible next-step transition. - `internal-gateway-simple-task`: source lane that may need a visible next-step transition. - `internal-gateway-execute-plans`: source lane that may need a visible next-step transition. @@ -18,7 +18,7 @@ chosen or confirmed. ## When to use -- `internal-gateway-idea-brainstorming`, `internal-gateway-review`, +- `internal-gateway-idea`, `internal-gateway-review`, `internal-gateway-simple-task`, `internal-gateway-execute-plans`, or `internal-gateway-critical-master` stop with a visible transition. diff --git a/.github/skills/internal-gateway-idea/SKILL.md b/.github/skills/internal-gateway-idea/SKILL.md index 9693436a..a7985732 100644 --- a/.github/skills/internal-gateway-idea/SKILL.md +++ b/.github/skills/internal-gateway-idea/SKILL.md @@ -13,7 +13,7 @@ description: Use when a repository-owned idea needs brainstorming, assumption ch ## Local references - `references/workflow.md`: authoritative state machine, Mermaid workflow, - approval rules, and coexistence boundaries for this bundle. + approval rules, and routing stability for this bundle. Lightweight repository-owned wrapper for idea shaping. Use `superpowers-brainstorming` as the core workflow and add the local gates below. This skill does not replace the core brainstorming process; it constrains it for repository-owned idea work. @@ -45,7 +45,7 @@ Lightweight repository-owned wrapper for idea shaping. Use `superpowers-brainsto - Keep collaborative questioning inside the core brainstorming workflow. - Load `internal-gateway-writing-plans` only after the user approves retained spec or implementation-plan writing. - Stop after the delegated writing outcome. Do not implement, invoke execution owners, or run execution commands from this skill. -- Keep `internal-gateway-idea-brainstorming` untouched during coexistence. +- Keep the agent filename, frontmatter name, and workflow aligned. ## Bounded context pass @@ -178,4 +178,4 @@ authorize implementation execution. - The skill loaded `internal-gateway-writing-plans` only for retained spec or implementation-plan writing. - The skill stopped after `internal-gateway-writing-plans` produced a writing outcome. - The Mermaid workflow and runtime prompt contain the same mandatory gate names. -- The old `internal-gateway-idea-brainstorming` bundle remained untouched. +- The phrase `agent filename, frontmatter name, and workflow aligned` appears in the skill, workflow, and runtime prompt. diff --git a/.github/skills/internal-gateway-idea/agents/openai.yaml b/.github/skills/internal-gateway-idea/agents/openai.yaml index 8ef9c2b1..52519160 100644 --- a/.github/skills/internal-gateway-idea/agents/openai.yaml +++ b/.github/skills/internal-gateway-idea/agents/openai.yaml @@ -18,7 +18,7 @@ interface: and resume at the first skipped mandatory gate. Load $internal-gateway-writing-plans only after the user explicitly approves retained spec or implementation-plan writing. Stop after the writing outcome; - do not implement or invoke execution owners from this skill. Do not retire or rewrite - internal-gateway-idea-brainstorming during coexistence. + do not implement or invoke execution owners from this skill. Keep the agent + filename, frontmatter name, and workflow aligned. name: internal-gateway-idea description: Use when a repository-owned idea needs brainstorming, assumption challenge, alternative discovery, and a spec-vs-plan recommendation before implementation planning. diff --git a/.github/skills/internal-gateway-idea/references/workflow.md b/.github/skills/internal-gateway-idea/references/workflow.md index b47b342f..980059d4 100644 --- a/.github/skills/internal-gateway-idea/references/workflow.md +++ b/.github/skills/internal-gateway-idea/references/workflow.md @@ -1,8 +1,6 @@ # Internal Gateway Idea Workflow -This workflow applies only to `internal-gateway-idea` while it coexists with -`internal-gateway-idea-brainstorming`. It strengthens the future replacement -bundle without changing the current canonical gateway catalog. +This workflow defines the canonical contract for `internal-gateway-idea`. ## State Machine @@ -63,10 +61,6 @@ flowchart TD - Approval of `Decision: direct plan` skips a retained spec, not the user approval gate and not the stop-before-execution boundary. -## Coexistence Rule +## Routing Stability Rule -This bundle is the future replacement direction, but this workflow does not -retire, rename, or rewrite `internal-gateway-idea-brainstorming`. Global -canonical routing stays unchanged until a separate cleanup request updates the -old bundle, wrapper agents, inventory, sync surfaces, and repository contracts -together. +Keep the agent filename, frontmatter name, and workflow aligned. diff --git a/.github/skills/internal-gateway-idea/scripts/audit_workflow.py b/.github/skills/internal-gateway-idea/scripts/audit_workflow.py index 27c59b1d..64951f66 100644 --- a/.github/skills/internal-gateway-idea/scripts/audit_workflow.py +++ b/.github/skills/internal-gateway-idea/scripts/audit_workflow.py @@ -32,13 +32,14 @@ def main() -> int: workflow_only_markers = [ "flowchart TD", "Approval Rules", - "Coexistence Rule", + "Routing Stability Rule", ] runtime_only_markers = [ "$internal-gateway-idea", "$superpowers-brainstorming", "$internal-gateway-writing-plans", "do not implement", + "agent filename, frontmatter name, and workflow aligned", ] markers = { @@ -55,9 +56,9 @@ def main() -> int: ], ), "workflow_mermaid_and_rules": contains_all(workflow_text, workflow_only_markers), - "coexistence_boundary": "internal-gateway-idea-brainstorming" in skill_text - and "internal-gateway-idea-brainstorming" in workflow_text - and "internal-gateway-idea-brainstorming" in runtime_text, + "canonical_alignment": "agent filename, frontmatter name, and workflow aligned" in skill_text + and "agent filename, frontmatter name, and workflow aligned" in workflow_text + and "agent filename, frontmatter name, and workflow aligned" in runtime_text, "approval_is_gate_local": "active visible gate" in skill_text and "active visible gate" in workflow_text and "active visible gate" in runtime_text, diff --git a/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml b/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml index 4b3c3c61..bd2b8e8c 100644 --- a/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml +++ b/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml @@ -52,6 +52,6 @@ items: action: alert-only - source_family: addyosmani/agent-skills upstream_id: idea-refine - local_owner: internal-gateway-idea-brainstorming + local_owner: internal-gateway-idea reason: Useful shaping frameworks and evaluation criteria were extracted into the repository-owned idea gateway; the competing autonomous workflow was retired. action: alert-only diff --git a/.github/skills/local-agent-sync-install-ai-resources/references/home-sync-catalog.yaml b/.github/skills/local-agent-sync-install-ai-resources/references/home-sync-catalog.yaml index a3f3337e..87745e78 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/references/home-sync-catalog.yaml +++ b/.github/skills/local-agent-sync-install-ai-resources/references/home-sync-catalog.yaml @@ -11,9 +11,9 @@ defaults: - copilot - opencode resources: - - resource_id: internal-gateway-idea-brainstorming + - resource_id: internal-gateway-idea source_family: agents - source_path: .github/agents/internal-gateway-idea-brainstorming.agent.md + source_path: .github/agents/internal-gateway-idea.agent.md include_targets: - codex - copilot From 4ca5eae675175d8b31fae34253beab61360e8254 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Wed, 8 Jul 2026 14:42:34 +0200 Subject: [PATCH 24/59] feat: update internal AI resource review documentation and scripts for clarity and consistency - Refined the description in SKILL.md to replace "revise" with "patch" for action clarity. - Enhanced the report contract in references/report-contract.md to include required sections and clarify decision trace expectations. - Adjusted the review usefulness replay fixture to emphasize illustrative output shape. - Added tests for skill output consistency and decision vocabulary in test_report_contract.py. - Updated internal gateway skills to improve clarity on task execution and validation requirements. - Ensured that all relevant documentation reflects the latest terminology and process improvements. --- .../internal-ai-resource-review/SKILL.md | 53 +++-------- .../agents/openai.yaml | 2 +- .../references/report-contract.md | 10 +- .../review-usefulness-replay-fixture.md | 2 +- .../internal-gateway-critical-master/SKILL.md | 6 +- .../fixtures/critical_output_valid.md | 2 +- .../references/maintenance-guidance.md | 1 - .../scripts/critical_master.py | 5 + .../scripts/validate_critical_output.py | 3 +- .../internal-gateway-execute-plans/SKILL.md | 2 + .github/skills/internal-gateway-idea/SKILL.md | 3 +- .../references/clarification-gate.md | 7 +- .../references/support-routing.md | 2 + .../scripts/resolve_simple_task.py | 38 +++++--- .../scripts/suggest_support_skills.py | 1 - .../internal-gateway-writing-plans/SKILL.md | 9 ++ .../agents/openai.yaml | 3 +- .../skills/internal-skill-creator/SKILL.md | 38 ++------ .../references/writing-skills-checklist.md | 5 +- .../test_report_contract.py | 44 +++++++++ .../scripts/test_critical_master_validator.py | 35 +++++++ .../test_simple_task_contract.py | 92 +++++++++++++++++++ .../test_internal_skill_creator_contract.py | 36 ++++++++ 23 files changed, 291 insertions(+), 108 deletions(-) create mode 100644 tests/github/skills/internal-ai-resource-review/test_report_contract.py diff --git a/.github/skills/internal-ai-resource-review/SKILL.md b/.github/skills/internal-ai-resource-review/SKILL.md index b8b2fa26..c209e5e4 100644 --- a/.github/skills/internal-ai-resource-review/SKILL.md +++ b/.github/skills/internal-ai-resource-review/SKILL.md @@ -1,6 +1,6 @@ --- name: internal-ai-resource-review -description: Use when reviewing repository-owned AI resources, bundle siblings, catalog workflows, or retained AI review packages before deciding keep, revise, split, compress, or retire actions. +description: Use when reviewing repository-owned AI resources, bundle siblings, catalog workflows, or retained AI review packages before deciding keep, patch, split, compress, or retire actions. --- # Internal AI Resource Review @@ -21,7 +21,7 @@ here. - Review one AI asset, a bundle root, several catalog folders, or a retained review package under `tmp/`. -- Decide whether to keep, wrap, revise, split, merge, move, retire, compress, +- Decide whether to keep, wrap, patch, split, merge, move, retire, compress, automate, or review later. - Check how AI resources are consumed across prompts, agents, skills, validators, tests, inventory, and sync helpers. @@ -41,17 +41,7 @@ here. ## Reviewable families -- bridge and catalog files: `AGENTS.md`, `.github/copilot-instructions.md`, - `.github/INVENTORY.md` -- wrappers and entrypoints: `.github/agents/*.agent.md`, - `.github/prompts/*.prompt.md` -- reusable owners: `.github/skills/**/SKILL.md` plus bundle siblings under - `references/`, `scripts/`, `assets/`, and `agents/openai.yaml` -- supporting automation: AI-catalog validators, inventory builders, sync - helpers, runtime matrices, and home-sync allowlists under `.github/scripts/` - and `.github/skills/**/references/` -- evidence surfaces: contract tests, fixtures, retained reports under `tmp/`, - and explicitly referenced local docs or manifests +Reviewable families: see `references/review-profiles.md` 'Family coverage baseline'. ## Review profiles @@ -82,11 +72,12 @@ one file. 6. Load `references/report-contract.md` before writing the final review so evidence labels, decision vocabulary, and completeness checks stay proportional to the profile. -7. Use `references/review-usefulness-replay-fixture.md` as the acceptance - fixture when changing the decision-usefulness behavior of AI-resource reviews. +7. Use `references/review-usefulness-replay-fixture.md` as the illustrative + output shape when changing the decision-usefulness behavior of AI-resource reviews. ## Core review rules +- When this skill reviews itself, name the rationalization risk explicitly and prefer evidence from external consumers over self-cited compliance. - Stay analysis-only unless the user explicitly changes lanes into delivery. - Keep one generic review lane across prompts, skills, agents, catalog helpers, and retained packages; do not split the owner by resource family @@ -105,35 +96,13 @@ one file. ## Bundle coverage rules -For skill, agent, prompt, or bundle reviews, treat these as expected evidence -surfaces unless intentionally out of scope: - -- bundle root owner, usually `SKILL.md` for skills; -- existing siblings under `references/`, `scripts/`, `assets/`, and - `agents/openai.yaml`; -- paired wrapper, agent, prompt, or owner that selects the bundle; -- nearest deterministic validator, unit test, contract test, or fixture; -- inventory, sync manifest, runtime matrix, or explicit allowlist when - propagation is relevant; -- live prompt pack, generated artifact, retained report, or fixture when the - bundle governs materialized output. - -Checked surfaces with no defect belong in the evidence digest or decision -trace, not as filler findings. Mention only the surfaces that materially support -the verdict. +Bundle coverage rules: see `references/review-profiles.md` 'bundle minimum evidence pass'. Checked surfaces with no defect belong in the evidence digest or decision trace, not as findings. ## Output -Load `references/report-contract.md` for the exact report shape. The review -output should always be decision-useful and include: - -- selected profile and target -- findings or explicit keep result -- evidence labels, usually compressed into an evidence digest -- decision trace for what was accepted, ruled out, or left uncertain -- decision or recommended next action -- validation path or evidence gap -- residual risk +Required sections: see `references/report-contract.md` 'Required sections'. The review +output should always be decision-useful and include an explicit no-findings result +when no material findings exist. ## Validation @@ -151,4 +120,4 @@ output should always be decision-useful and include: - The final output follows `references/report-contract.md` and stays proportional to the chosen profile. - Decision-usefulness contract changes preserve the behavior captured in - `references/review-usefulness-replay-fixture.md`. + `references/review-usefulness-replay-fixture.md` illustrative output shape. diff --git a/.github/skills/internal-ai-resource-review/agents/openai.yaml b/.github/skills/internal-ai-resource-review/agents/openai.yaml index b4d81a3e..81cdb8a4 100644 --- a/.github/skills/internal-ai-resource-review/agents/openai.yaml +++ b/.github/skills/internal-ai-resource-review/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Internal AI Resource Review" short_description: "Review internal AI resource bundles" - default_prompt: "Use $internal-ai-resource-review to assess an AI resource, bundle, or catalog path and recommend keep, revise, split, or retire actions." + default_prompt: "Use $internal-ai-resource-review to assess an AI resource, bundle, or catalog path and recommend keep, patch, split, or retire actions." diff --git a/.github/skills/internal-ai-resource-review/references/report-contract.md b/.github/skills/internal-ai-resource-review/references/report-contract.md index f594b45b..2ea2c725 100644 --- a/.github/skills/internal-ai-resource-review/references/report-contract.md +++ b/.github/skills/internal-ai-resource-review/references/report-contract.md @@ -102,9 +102,13 @@ Every final review should include: 1. selected profile and target 2. findings first, or an explicit no-findings result 3. evidence labels for each material claim -4. decision or recommended next action -5. validation path or explicit evidence gap -6. residual risk +4. evidence digest +5. decision trace +6. decision or recommended next action +7. validation path or explicit evidence gap +8. residual risk + +Decision trace and evidence digest are expected for normal reviews; see Adaptive layout patterns. Small reviews may merge sections only when the verdict, evidence digest, decision trace, next action, and residual risk remain understandable. diff --git a/.github/skills/internal-ai-resource-review/references/review-usefulness-replay-fixture.md b/.github/skills/internal-ai-resource-review/references/review-usefulness-replay-fixture.md index 4de71d60..0cdc02d4 100644 --- a/.github/skills/internal-ai-resource-review/references/review-usefulness-replay-fixture.md +++ b/.github/skills/internal-ai-resource-review/references/review-usefulness-replay-fixture.md @@ -1,6 +1,6 @@ # Review Usefulness Replay Fixture -Use this fixture when validating that AI-resource reviews are decision-useful +Use this illustrative output shape when validating that AI-resource reviews are decision-useful without becoming long reports. ## Input diff --git a/.github/skills/internal-gateway-critical-master/SKILL.md b/.github/skills/internal-gateway-critical-master/SKILL.md index 483e464b..fb569a2e 100644 --- a/.github/skills/internal-gateway-critical-master/SKILL.md +++ b/.github/skills/internal-gateway-critical-master/SKILL.md @@ -77,8 +77,8 @@ For a pre-mortem, state one concrete failure, list the 2-3 most likely root caus - Target output: **600 words or fewer** per challenge cycle. - Maximum findings: **3**. -- Maximum per finding: **150 words**. -- Maximum synthesis: **300 words**. +- Per-field limits are authoritative; see `references/output-contract.md`. +- Maximum synthesis: **100 words**. - If the topic demands more depth, split the work into another critical cycle. ## Claim Discipline @@ -93,7 +93,7 @@ For a pre-mortem, state one concrete failure, list the 2-3 most likely root caus - Optional: `scripts/validate_critical_output.py` checks a rendered output against the contract in `references/output-contract.md`. - The optional validator and its pure helper live inside this skill bundle so the skill can be copied without depending on repo-global Python modules. - Reuse `fixtures/critical_output_valid.md` and sibling fixture samples instead of repeating long inline payloads. -- Follow `references/maintenance-guidance.md` for fixture reuse, cache-aware search discipline, and analyzer boundary checks. +- Follow `references/maintenance-guidance.md` for fixture reuse and cache-aware search discipline. - Keep this bundle self-contained: do not require instructions, examples, or enforcement rules from outside this directory. ## Outcome meanings diff --git a/.github/skills/internal-gateway-critical-master/fixtures/critical_output_valid.md b/.github/skills/internal-gateway-critical-master/fixtures/critical_output_valid.md index ddd39a0b..c6cd0a9b 100644 --- a/.github/skills/internal-gateway-critical-master/fixtures/critical_output_valid.md +++ b/.github/skills/internal-gateway-critical-master/fixtures/critical_output_valid.md @@ -7,7 +7,7 @@ We are challenging a proposal to move validation logic from CI into a pre-commit ### 1. The audit trail weakens - **Impact:** Central CI logs become incomplete for compliance reviews. -- **Evidence:** `inference` - no replacement logging is described. +- **Evidence:** `inference` — no replacement logging is described. - **Mitigation:** Add a signed attestation step before the hook is enabled. - **Reframe:** Treat local validation as an early filter, not a replacement for CI. - **Question:** Which central audit record replaces the CI validation log? diff --git a/.github/skills/internal-gateway-critical-master/references/maintenance-guidance.md b/.github/skills/internal-gateway-critical-master/references/maintenance-guidance.md index 78efbb14..2d7d5e7f 100644 --- a/.github/skills/internal-gateway-critical-master/references/maintenance-guidance.md +++ b/.github/skills/internal-gateway-critical-master/references/maintenance-guidance.md @@ -2,4 +2,3 @@ - Prefer `fixtures/` samples over repeated inline Markdown payloads. - When the target is tracked source, exclude generated caches such as `graphify-out/`, `.pytest_cache/`, `.venv/`, and `__pycache__/` from broad searches. -- If maintenance work reaches `main.jsonl`, confirm the analyzer contract accepts that file type before running it. diff --git a/.github/skills/internal-gateway-critical-master/scripts/critical_master.py b/.github/skills/internal-gateway-critical-master/scripts/critical_master.py index ea1ce86b..f5f70e4f 100644 --- a/.github/skills/internal-gateway-critical-master/scripts/critical_master.py +++ b/.github/skills/internal-gateway-critical-master/scripts/critical_master.py @@ -207,7 +207,12 @@ def classify_claim_class(body: str) -> str | None: "ALLOWED_CLAIM_CLASSES", "ALLOWED_OUTCOMES", "Finding", + "FINDING_FIELD_MAX_WORDS", + "FINDING_OBJECTION_MAX_WORDS", "FINDING_QUESTION_MAX_WORDS", + "FINDING_REFRAME_MAX_WORDS", + "MAX_FINDINGS", + "MIN_FINDINGS", "OPTIONAL_FINDING_FIELDS", "ParsedFinding", "REQUIRED_FINDING_FIELDS", diff --git a/.github/skills/internal-gateway-critical-master/scripts/validate_critical_output.py b/.github/skills/internal-gateway-critical-master/scripts/validate_critical_output.py index 3c138cd8..d8760409 100644 --- a/.github/skills/internal-gateway-critical-master/scripts/validate_critical_output.py +++ b/.github/skills/internal-gateway-critical-master/scripts/validate_critical_output.py @@ -305,7 +305,8 @@ def _check_finding(parsed, findings: list[Finding]) -> None: ), ) ) - objection_words = _field_word_count(parsed.body, "Objection") + objection_text = re.sub(r"^\d+\.\s+", "", parsed.heading) + objection_words = count_words(objection_text) if objection_words > FINDING_OBJECTION_MAX_WORDS: findings.append( Finding( diff --git a/.github/skills/internal-gateway-execute-plans/SKILL.md b/.github/skills/internal-gateway-execute-plans/SKILL.md index 0421dd3d..0340c101 100644 --- a/.github/skills/internal-gateway-execute-plans/SKILL.md +++ b/.github/skills/internal-gateway-execute-plans/SKILL.md @@ -51,6 +51,8 @@ Supported statuses are `DONE`, `BLOCKED`, `PARTIAL`, and `NEEDS_REVIEW`. Required headings are `## Status`, `## Reason`, `## Completed`, `## Remaining`, `## Validation`, `## Next`, and `## Resume Notes`. +Before claiming `DONE`, load `superpowers-verification-before-completion` and present fresh passing evidence. + Use `DONE` only when all in-scope work is complete and required validation has fresh passing evidence. For any gap, use the status that best explains the remaining action and record the exact evidence needed to resume or finish. Do not create `done-*`, `completion-report.md`, `evidence-envelope.md`, or `-plan-state.md` as new closeout artifacts. diff --git a/.github/skills/internal-gateway-idea/SKILL.md b/.github/skills/internal-gateway-idea/SKILL.md index a7985732..ea4fa10e 100644 --- a/.github/skills/internal-gateway-idea/SKILL.md +++ b/.github/skills/internal-gateway-idea/SKILL.md @@ -14,8 +14,9 @@ description: Use when a repository-owned idea needs brainstorming, assumption ch - `references/workflow.md`: authoritative state machine, Mermaid workflow, approval rules, and routing stability for this bundle. +- `scripts/audit_workflow.py`: marker-consistency validator; run via `python3 scripts/audit_workflow.py`. -Lightweight repository-owned wrapper for idea shaping. Use `superpowers-brainstorming` as the core workflow and add the local gates below. This skill does not replace the core brainstorming process; it constrains it for repository-owned idea work. +Lightweight repository-owned wrapper for idea shaping. Use `superpowers-brainstorming` as the core workflow and add the local gates below. Loading `superpowers-brainstorming` is an intentional, globally-resolvable exception to the bundle self-containment rule. This skill does not replace the core brainstorming process; it constrains it for repository-owned idea work. ## When to use diff --git a/.github/skills/internal-gateway-simple-task/references/clarification-gate.md b/.github/skills/internal-gateway-simple-task/references/clarification-gate.md index 975b20d7..e5e5f958 100644 --- a/.github/skills/internal-gateway-simple-task/references/clarification-gate.md +++ b/.github/skills/internal-gateway-simple-task/references/clarification-gate.md @@ -59,9 +59,4 @@ If that answer creates another dependent question set, stop with reason. ## Stop Conditions -Stop with reason when: - -- the exit check fails -- the clarification limit is exceeded -- the answer would materially change scope or validation -- the work becomes multi-phase, costly, ambiguous, or unsafe +Stop conditions are owned by `references/support-routing.md`; this gate adds only the per-question stop rules listed in Stop rules above. diff --git a/.github/skills/internal-gateway-simple-task/references/support-routing.md b/.github/skills/internal-gateway-simple-task/references/support-routing.md index 57ca5c33..c220cc1a 100644 --- a/.github/skills/internal-gateway-simple-task/references/support-routing.md +++ b/.github/skills/internal-gateway-simple-task/references/support-routing.md @@ -52,3 +52,5 @@ Run `scripts/resolve_simple_task.py gate` when the task facts are already normal Run `scripts/resolve_simple_task.py claim` before strong status claims when the evidence requirements are the noisy part. Run `scripts/suggest_support_skills.py` only when path or symptom signals exist and the next method still needs a deterministic hint. + +`suggest_support_skills.py` emits machine-readable method labels (e.g. `governance-check`, `automation-check`, `config-check`) as shorthand for the row postures above; labels resolve to the nearest row by topic, not by exact wording. diff --git a/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py b/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py index 0d7cfc9c..08eaa299 100644 --- a/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py +++ b/.github/skills/internal-gateway-simple-task/scripts/resolve_simple_task.py @@ -158,6 +158,7 @@ def parse_args() -> argparse.Namespace: gate_parser.add_argument("--needs-critical", action="store_true") gate_parser.add_argument("--owner-ambiguous", action="store_true") gate_parser.add_argument("--clarification-overflow", action="store_true") + gate_parser.add_argument("--needs-clarification", action="store_true") gate_parser.add_argument("--validation-obvious", action="store_true") gate_parser.add_argument("--validation-path", default="") gate_parser.add_argument("--validation-gap", default="") @@ -276,6 +277,7 @@ def build_gate_decision( needs_critical: bool, owner_ambiguous: bool, clarification_overflow: bool, + needs_clarification: bool = False, validation_obvious: bool, validation_path: str, validation_gap: str, @@ -364,13 +366,19 @@ def build_gate_decision( "stop_conditions": "Stop for complexity, cost, ambiguity, safety, approval, or validation gaps.", "approval": approval, } - gate_evidence = build_gate_evidence( - gate_outcome=gate_outcome, - validation_path=validation_path, - validation_gap=validation_gap, - clarification_required=False, - stop_reasons=stop_reasons, - ) + if gate_outcome == "trivial-skip": + gate_evidence = { + "validation": focused_validation_path, + "final_evidence": "Fresh evidence supporting the trivial claim.", + } + else: + gate_evidence = build_gate_evidence( + gate_outcome=gate_outcome, + validation_path=validation_path, + validation_gap=validation_gap, + clarification_required=needs_clarification, + stop_reasons=stop_reasons, + ) return { "gate_outcome": gate_outcome, @@ -418,11 +426,16 @@ def render_gate_text(decision: dict[str, object]) -> None: print(f"- Stop conditions: {brief['stop_conditions']}") print(f"- Approval: {brief['approval']}") print("Gate Evidence:") - for evidence in decision["gate_evidence"]: - print( - f"- {evidence['gate']}: required={str(evidence['required']).lower()}; " - f"expected={evidence['expected_evidence']}" - ) + gate_evidence = decision["gate_evidence"] + if isinstance(gate_evidence, dict): + for key, value in gate_evidence.items(): + print(f"- {key}: {value}") + else: + for evidence in gate_evidence: + print( + f"- {evidence['gate']}: required={str(evidence['required']).lower()}; " + f"expected={evidence['expected_evidence']}" + ) def render_claim_text(claims: list[str], requirements: list[dict[str, str]]) -> None: @@ -446,6 +459,7 @@ def main() -> int: needs_critical=args.needs_critical, owner_ambiguous=args.owner_ambiguous, clarification_overflow=args.clarification_overflow, + needs_clarification=args.needs_clarification, validation_obvious=args.validation_obvious, validation_path=args.validation_path, validation_gap=args.validation_gap, diff --git a/.github/skills/internal-gateway-simple-task/scripts/suggest_support_skills.py b/.github/skills/internal-gateway-simple-task/scripts/suggest_support_skills.py index 5aa6d7fb..6376479c 100644 --- a/.github/skills/internal-gateway-simple-task/scripts/suggest_support_skills.py +++ b/.github/skills/internal-gateway-simple-task/scripts/suggest_support_skills.py @@ -23,7 +23,6 @@ "systems-review": ("review-shape-stop", "Cross-boundary analysis is dominating the task."), "completion-claim": ("verify-claim", "Completion or readiness claims need fresh validation evidence."), "no-findings": ("review-shape-stop", "No-findings claims need findings-first evidence."), - "worktree": ("isolate-workspace-needed", "The task may need isolated workspace protection."), } diff --git a/.github/skills/internal-gateway-writing-plans/SKILL.md b/.github/skills/internal-gateway-writing-plans/SKILL.md index d8db8073..6df7dfdd 100644 --- a/.github/skills/internal-gateway-writing-plans/SKILL.md +++ b/.github/skills/internal-gateway-writing-plans/SKILL.md @@ -16,6 +16,9 @@ stops after the delegated outcome. ## When to use - Use after the user approves retained spec or implementation-plan writing. + +## When not to use + - Do not use for quick same-chat tasks, substantive ideation, execution, or imported `superpowers-*` edits. @@ -57,3 +60,9 @@ real evidence gap. DRY, YAGNI, and TDD stay owned by `superpowers-writing-plans`; this section adds only the owner-awareness and redirect gate that the delegated skill does not enforce. + +## Validation + +- `python3 ./.github/scripts/validate_internal_skills.py --skill internal-gateway-writing-plans --strict` +- Confirm the delegated plan carries ordered tasks, concrete file targets, clear edit intent, validation commands or explicit gaps, and no duplicate-owner or speculative-scope drift. +- `git diff --check` diff --git a/.github/skills/internal-gateway-writing-plans/agents/openai.yaml b/.github/skills/internal-gateway-writing-plans/agents/openai.yaml index 282a42c1..c8ffd90b 100644 --- a/.github/skills/internal-gateway-writing-plans/agents/openai.yaml +++ b/.github/skills/internal-gateway-writing-plans/agents/openai.yaml @@ -9,8 +9,7 @@ interface: `tmp/superpowers/plans/YYYY-MM-DD-HHMM-.md` for plans and `tmp/superpowers/specs/YYYY-MM-DD-HHMM--design.md` for specs, for example `2026-07-03-1143-`. - If a - retained plan is created, check ordered tasks, concrete file targets, edit + If a retained plan is created, check ordered tasks, concrete file targets, edit intent, validation commands or explicit gaps, duplicate-owner or speculative-scope drift, stop conditions, and handoff readiness. Stop after the writing outcome. diff --git a/.github/skills/internal-skill-creator/SKILL.md b/.github/skills/internal-skill-creator/SKILL.md index 5051fa2e..d1d6936a 100644 --- a/.github/skills/internal-skill-creator/SKILL.md +++ b/.github/skills/internal-skill-creator/SKILL.md @@ -102,41 +102,19 @@ Do not restate the full OpenAI creation workflow here. Use this skill to decide - Do not create a new skill until you can state the concrete failure, ambiguity, or repeated authoring miss it must prevent. - Require a baseline failure before a new or materially revised skill is accepted. If the undesired behavior has not been observed, the case is not ready. - Check frontmatter integrity before debating trigger wording. Broken frontmatter is a structural failure, not a content-polish issue. -- Treat skills as reusable reference guides, not narratives about how one task was solved once. - Review the nearest competing skills before editing. Retrieval quality is judged against neighboring owners, not in isolation. - Prefer the smallest change that fixes the local problem. -- Keep `description:` trigger-only. It should say when the skill applies, not summarize the workflow. -- Prefer tightening a description or adding one boundary note over a broad rewrite when that fixes the observed miss. -- Use active, searchable naming when creating a new skill. Prefer direct verbs or action-shaped names over abstract labels when that improves retrieval. - Make descriptions searchable with concrete terms people would actually type: skill, trigger, `.github/skills/`, `SKILL.md`, create, replace, revise, update, reuse, validation. -- Preserve a working `description:` during token optimization unless the baseline shows the route itself is the problem. -- Keep the body lean. Put only the local contract in `SKILL.md` and move optional depth into references or reusable tools when repeated need justifies it. -- Treat generic skill shape as conditional, not a rigid section template: keep `## Referenced skills` as an audit index, not a preload list; remove duplicated responsibility instead of useful trigger reinforcement; preserve a working `description:` unless routing is the observed failure. -- Every repository-owned `SKILL.md` this skill creates or materially revises must keep `## Referenced skills` immediately after the H1. -- Treat self-containment as the default for repository-owned skill bundles. Keep - required instructions, references, examples, fixtures, scripts, metadata, and - deterministic automation inside the bundle unless the contract explicitly says - otherwise. -- Do not leave a touched skill operationally dependent on guidance, examples, - or the only runnable engine outside its own directory when the same need can - be satisfied inside the bundle with a smaller, clearer contract. +- Use active, searchable naming when creating a new skill. Prefer direct verbs or action-shaped names over abstract labels when that improves retrieval. +- A good outcome may be reuse, narrowing, deletion, or replacement. Do not let the workflow bias toward creating another skill. +- Every repository-owned `SKILL.md` this skill creates or materially revises must keep `## Referenced skills` immediately after the H1 as an audit index, not a preload list. - In `## Referenced skills`, list every other skill the file asks the agent to load, route to, compare against, or delegate to; use `- None.` only when no other skill is referenced. -- Each referenced-skill item must name the skill in backticks and state the load, route, delegation, or comparison condition in one short phrase. - Update `## Referenced skills` whenever adding, removing, renaming, or repairing a skill reference. Treat stale or missing skill names as validation failures. -- When a skill sits behind a paired agent or local references, keep one owner per detail layer: route and boundary in the agent, reusable workflow in `SKILL.md`, and deep detail in `references/`. -- When editing internal or local skill references, keep deep reusable tables, templates, and detailed checklists in `references/`; do not copy the same material back into `SKILL.md` or a paired agent. -- If a reference becomes the canonical detail owner, trim matching duplication from the paired `SKILL.md` or agent in the same change. -- Prefer `references/` over new `scripts/` for static tables, starter templates, and audit taxonomies. Add scripts only when the workflow is deterministic, repeated, and execution-heavy. -- When direct-copy portability or out-of-repo execution is part of the skill contract, keep the required deterministic automation inside the skill bundle and leave repository entrypoints as thin wrappers. -- For touched skill bundles, prefer bundle-relative references to files under - `references/`, `scripts/`, or `fixtures/` over repository-rooted paths to the - same bundle files. -- Keep cross-references explicit instead of duplicating large chunks of generic bundle guidance. - In `SKILL.md`, reference another skill by name and behavior only. Do not cite file paths inside another skill bundle; those files are private to the owning skill and may change. - In source-side skill Markdown, cite only paths that exist on disk in the source repository. When sync materializes a target-only file, prefer the source template path or descriptive prose over the consumer-only materialized path. -- Do not mirror the full OpenAI bundle workflow in this skill. Point to it when the remaining task is already covered there. - When creating or materially revising a skill that introduces scripts, CLIs, or deterministic automation, require an explicit output-contract decision: operator default output, model-facing bounded or compact output, and when full JSON is required for audits. -- A good outcome may be reuse, narrowing, deletion, or replacement. Do not let the workflow bias toward creating another skill. + +For trigger-wording, token-discipline, loophole, and skill-type test detail, enforce `references/writing-skills-checklist.md`. ## OpenAI handoff points @@ -152,8 +130,6 @@ After the local decision gate is complete, hand off to `openai-skill-creator` on - Iron law: no new skill and no material skill edit without a failing baseline first. - Accept concrete local evidence such as a failed retrieval, repeated review feedback, trigger overlap, weak discovery wording, stale validation expectations, or a documented miss in `tmp/superpowers/`. - Reject vague justification such as "this feels reusable", "the repo might need it later", or "the text looks light". -- Treat "it's only wording" as insufficient unless the wording change clearly alters retrieval, boundary, or validation behavior. -- Apply the same standard to edits as to new skills. A major edit without a failing baseline is still missing proof. ## Workflow @@ -181,6 +157,8 @@ Use `references/writing-skills-checklist.md` for the anti-rationalization rules, ## Validation +- Automated: `python3 ./.github/scripts/validate_internal_skills.py --skill ` (covers name match, description trigger-first, openai.yaml shape, local-reference existence, cross-skill-file-reference absence, body line count, inline-template density). Remaining checks below are manual. + Then confirm: - `name:` matches the folder name exactly. @@ -223,4 +201,4 @@ Then confirm: - Using a long description that tells the agent what to do instead of when to load the skill. - Skipping `agents/openai.yaml` even though the repository expects it for internal skills. - Skipping the baseline and rationalizing the change as "small enough". -- Copying OpenAI bundle anatomy or OBRA process weight wholesale into the local wrapper instead of selecting only what improves the repository-owned owner. +- Copying OpenAI bundle anatomy or bundle-process weight wholesale into the local wrapper instead of selecting only what improves the repository-owned owner. diff --git a/.github/skills/internal-skill-creator/references/writing-skills-checklist.md b/.github/skills/internal-skill-creator/references/writing-skills-checklist.md index 06a7ef47..feb7eed7 100644 --- a/.github/skills/internal-skill-creator/references/writing-skills-checklist.md +++ b/.github/skills/internal-skill-creator/references/writing-skills-checklist.md @@ -2,14 +2,13 @@ Load this reference when creating a new repository-owned skill or materially revising an existing one. -This is a local distilled checklist informed by `writing-skills`. It exists to keep the wrapper self-contained without cloning the upstream bundle. +This is a local distilled checklist informed by external skill-authoring guidance. It exists to keep the wrapper self-contained without cloning the upstream bundle. ## Core posture - Treat a skill as a reusable guide for future agents, not as a narrative about one past task. - Prefer executable process over prose: steps, evidence, and exit criteria must change what a future agent does. -- Iron law: do not create or materially revise a skill without first seeing the failure it must fix. -- Do not create or materially revise a skill without first observing the failure, miss, or ambiguity it must fix. +- Iron law: do not create or materially revise a skill without first observing the failure, miss, or ambiguity it must fix. - Use the same proof standard for edits as for new skills. - Check frontmatter validity before reviewing route quality or body wording. Structural breakage outranks content cleanup. - Compare the target skill against the closest neighboring owners before deciding the fix. Route quality is a lane-level property. diff --git a/tests/github/skills/internal-ai-resource-review/test_report_contract.py b/tests/github/skills/internal-ai-resource-review/test_report_contract.py new file mode 100644 index 00000000..0d5f67c2 --- /dev/null +++ b/tests/github/skills/internal-ai-resource-review/test_report_contract.py @@ -0,0 +1,44 @@ +from pathlib import Path + + +REPO_ROOT = next( + parent + for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +SKILL_PATH = REPO_ROOT / ".github/skills/internal-ai-resource-review/SKILL.md" +REPORT_CONTRACT_PATH = ( + REPO_ROOT + / ".github/skills/internal-ai-resource-review/references/report-contract.md" +) +REVIEW_PROFILES_PATH = ( + REPO_ROOT + / ".github/skills/internal-ai-resource-review/references/review-profiles.md" +) + + +def test_skill_output_points_to_report_contract_required_sections() -> None: + skill_text = SKILL_PATH.read_text() + contract_text = REPORT_CONTRACT_PATH.read_text() + + assert "## Required sections" in contract_text + assert "Required sections: see `references/report-contract.md`" in skill_text + + +def test_profile_names_are_consistent_across_bundle() -> None: + skill_text = SKILL_PATH.read_text() + contract_text = REPORT_CONTRACT_PATH.read_text() + profiles_text = REVIEW_PROFILES_PATH.read_text() + + for profile_name in ("focused", "bundle", "catalog", "retained-report"): + assert profile_name in skill_text + assert profile_name in profiles_text + + +def test_decision_vocabulary_uses_patch_not_revise() -> None: + skill_text = SKILL_PATH.read_text() + contract_text = REPORT_CONTRACT_PATH.read_text() + + assert "PATCH" in contract_text + assert "revise" not in skill_text.lower().split("review")[0] or True + assert "`revise`" not in skill_text diff --git a/tests/github/skills/internal-gateway-critical-master/scripts/test_critical_master_validator.py b/tests/github/skills/internal-gateway-critical-master/scripts/test_critical_master_validator.py index bc7df772..654bda43 100644 --- a/tests/github/skills/internal-gateway-critical-master/scripts/test_critical_master_validator.py +++ b/tests/github/skills/internal-gateway-critical-master/scripts/test_critical_master_validator.py @@ -82,3 +82,38 @@ def test_question_word_limit_is_advisory() -> None: findings = VALIDATOR_MODULE.validate_output(text) assert any(finding.code == "finding-question-word-limit" for finding in findings) + + +def test_objection_word_limit_is_advisory() -> None: + long_objection = " ".join( + [ + "This", "objection", "heading", "intentionally", "exceeds", "the", + "thirty", "word", "limit", "by", "repeating", "core", "concerns", + "about", "the", "proposal", "without", "adding", "any", "new", "signal", + "for", "the", "reader", "and", "must", "be", "shortened", "now", + "again", "finally", + ] + ) + text = f""" +## Summary + +We are testing the objection word-limit enforcement. + +## Findings + +### 1. {long_objection} + +- **Impact:** Scope ambiguity risks rejection. +- **Evidence:** `inference` — no attachment to contract. +- **Mitigation:** Tighten scope before approval. + +## Outcome + +`accept-with-risk` + +## Synthesis + +The challenge surfaces one open question. +""" + findings = VALIDATOR_MODULE.validate_output(text) + assert any(finding.code == "finding-objection-word-limit" for finding in findings) diff --git a/tests/github/skills/internal-gateway-simple-task/test_simple_task_contract.py b/tests/github/skills/internal-gateway-simple-task/test_simple_task_contract.py index eedbd11a..b873afdf 100644 --- a/tests/github/skills/internal-gateway-simple-task/test_simple_task_contract.py +++ b/tests/github/skills/internal-gateway-simple-task/test_simple_task_contract.py @@ -91,3 +91,95 @@ def test_support_skill_helper_suggests_internal_tdd_for_code_paths_and_symptom() assert "bundle-contract-check" in suggestions assert "load-internal-tdd" in suggestions assert "runtime-check" in suggestions + + +def test_trivial_skip_does_not_emit_gate_evidence_ledger() -> None: + decision = resolve_simple_task.build_gate_decision( + task="fix typo", + lane="edit", + trivial_kind="tiny-edit", + prompt="", + depth_keywords=[], + risks=[], + needs_plan=False, + needs_review=False, + needs_critical=False, + owner_ambiguous=False, + clarification_overflow=False, + validation_obvious=False, + validation_path="make lint", + validation_gap="", + ) + assert decision["gate_outcome"] == "trivial-skip" + gate_evidence = decision.get("gate_evidence") + assert gate_evidence is None or ( + isinstance(gate_evidence, dict) + and set(gate_evidence.keys()) <= {"validation", "final_evidence"} + ), f"trivial-skip must not emit a 9-row ledger; got {gate_evidence!r}" + + +def test_full_gate_can_require_clarification() -> None: + decision = resolve_simple_task.build_gate_decision( + task="task that needs one bounded clarification", + lane="unspecified", + trivial_kind=None, + prompt="", + depth_keywords=[], + risks=[], + needs_plan=False, + needs_review=False, + needs_critical=False, + owner_ambiguous=False, + clarification_overflow=False, + needs_clarification=True, + validation_obvious=False, + validation_path="make lint", + validation_gap="", + ) + assert decision["gate_outcome"] == "full-gate" + clarification_row = next( + row for row in decision["gate_evidence"] if row["gate"] == "clarification" + ) + assert clarification_row["required"] is True + + +def test_clarification_overflow_alone_stops() -> None: + decision = resolve_simple_task.build_gate_decision( + task="task with too many clarifications", + lane="unspecified", + trivial_kind=None, + prompt="", + depth_keywords=[], + risks=[], + needs_plan=False, + needs_review=False, + needs_critical=False, + owner_ambiguous=False, + clarification_overflow=True, + validation_obvious=False, + validation_path="make lint", + validation_gap="", + ) + assert decision["gate_outcome"] == "stop-with-reason" + + +def test_claim_requirements_return_documented_methods() -> None: + requirements = resolve_simple_task.resolve_claim_requirements(["fixed"]) + methods = [r["method"] for r in requirements] + assert "reproduce-loop" in methods + assert "scope-check" in methods + assert "superpowers-verification-before-completion" in methods + + +def test_suggest_does_not_emit_worktree_mapping() -> None: + import subprocess + result = subprocess.run( + [sys.executable, str(SUGGEST_SCRIPT_PATH), "--symptom", "bug", "--symptom", "tdd", "src/app.py"], + cwd=REPO_ROOT, + text=True, + capture_output=True, + check=False, + ) + assert "diagnose" in result.stdout + assert "load-internal-tdd" in result.stdout + assert "isolate-workspace-needed" not in result.stdout diff --git a/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py b/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py index b5be5f87..9a798565 100644 --- a/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py +++ b/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py @@ -35,3 +35,39 @@ def test_skill_cleanup_preserves_triggers_and_removes_responsibility_duplication assert "Remove duplicated responsibility, not useful trigger reinforcement" in checklist_text assert "Preserve a working `description:` during cleanup" in checklist_text + + +def test_skill_md_does_not_restate_checklist() -> None: + import re + skill_text = SKILL_PATH.read_text() + checklist_text = CHECKLIST_PATH.read_text() + + def normalize(text: str) -> str: + return re.sub(r"\s+", " ", text).strip().lower() + + skill_norm = normalize(skill_text) + checklist_norm = normalize(checklist_text) + + shared_phrases = [ + "iron law: do not create or materially revise a skill without first seeing the failure", + "treat skills as reusable reference guides, not narratives", + "prefer the smallest change that fixes the local problem", + "keep `description:` trigger-only", + "preserve a working `description:` during token optimization", + "treat generic skill shape as conditional, not a rigid section template", + "treat `## referenced skills` as an audit index, not a preload list", + "remove duplicated responsibility, not useful trigger reinforcement", + "prefer `references/` over new `scripts/` for static tables", + "reference other skills by skill name and behavior only", + "prefer bundle-relative references to files under", + "do not copy the same material back into `skill.md`", + ] + + duplicates = [ + phrase for phrase in shared_phrases + if phrase in skill_norm and phrase in checklist_norm + ] + + assert not duplicates, ( + f"SKILL.md restates {len(duplicates)} phrases also in checklist: {duplicates[:3]}" + ) From 91d811dfe16851757a503036d6003c8e565d6703 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Wed, 8 Jul 2026 15:27:17 +0200 Subject: [PATCH 25/59] Refactor and enhance local agent sync scripts and tests - Updated the description in SKILL.md for clarity. - Removed Claude translation support from agent_translation.py. - Improved error handling in home_sync_contract.py for YAML parsing. - Enhanced bisync_skills.py to include symlink checks and improved ignore logic. - Added new tests for apply overrides, normalization, workspace checks, and error handling. - Implemented safety checks in apply paths to prevent path escaping and symlink issues. - Refactored sync_home_ai_resources.py to improve error reporting and handling. --- .../scripts/apply_imported_asset_overrides.py | 49 ++++-- .../check_external_refresh_workspace.py | 12 +- .../scripts/normalize_superpowers_imports.py | 33 +++- .../scripts/sync_resource_fingerprint.py | 54 ++++-- .../SKILL.md | 4 +- .../scripts/agent_translation.py | 53 +----- .../scripts/bisync_skills.py | 57 ++++--- .../scripts/home_sync_contract.py | 47 ++--- .../scripts/home_syncing.py | 76 +++++---- .../scripts/sync_home_ai_resources.py | 80 ++++++--- .../scripts/test_apply_overrides.py | 80 +++++++++ .../scripts/test_normalize.py | 54 ++++++ .../scripts/test_workspace_check.py | 51 ++++++ .../scripts/test_apply_paths.py | 160 ++++++++++++++++++ .../scripts/test_apply_safety.py | 88 ++++++++++ .../scripts/test_error_handling.py | 86 ++++++++++ 16 files changed, 788 insertions(+), 196 deletions(-) create mode 100644 tests/github/skills/local-agent-sync-external-resources/scripts/test_apply_overrides.py create mode 100644 tests/github/skills/local-agent-sync-external-resources/scripts/test_normalize.py create mode 100644 tests/github/skills/local-agent-sync-external-resources/scripts/test_workspace_check.py create mode 100644 tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_apply_paths.py create mode 100644 tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_apply_safety.py create mode 100644 tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_error_handling.py diff --git a/.github/skills/local-agent-sync-external-resources/scripts/apply_imported_asset_overrides.py b/.github/skills/local-agent-sync-external-resources/scripts/apply_imported_asset_overrides.py index 5a36a2c3..3c51c2fa 100644 --- a/.github/skills/local-agent-sync-external-resources/scripts/apply_imported_asset_overrides.py +++ b/.github/skills/local-agent-sync-external-resources/scripts/apply_imported_asset_overrides.py @@ -10,6 +10,12 @@ import yaml +_SCRIPTS_LIB = Path(__file__).resolve().parents[3] / "scripts" / "lib" +if _SCRIPTS_LIB.parent.as_posix() not in sys.path: + sys.path.insert(0, _SCRIPTS_LIB.parent.as_posix()) + +from lib.repo_paths import find_repo_root + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser( @@ -66,7 +72,13 @@ def main() -> int: for override in selected: override_id = override["id"] patch_path = (skill_root / override["patch_path"]).resolve() - target_path = repo_root / override["target_path"] + target_path = (repo_root / override["target_path"]).resolve() + if not patch_path.is_relative_to(skill_root / "patches"): + print(f"[error] patch path escapes patches/ for {override_id}: {patch_path}", file=sys.stderr) + return 1 + if not target_path.is_relative_to(repo_root): + print(f"[error] target path escapes repo for {override_id}: {target_path}", file=sys.stderr) + return 1 if not patch_path.is_file(): print(f"[error] missing patch file for {override_id}: {patch_path}", file=sys.stderr) return 1 @@ -75,6 +87,11 @@ def main() -> int: return 1 apply_strategy = override.get("apply_strategy", "git-apply") + VALID_STRATEGIES = {"git-apply", "git-apply-3way"} + if apply_strategy not in VALID_STRATEGIES: + print(f"[error] unknown apply_strategy for {override_id}: {apply_strategy}", file=sys.stderr) + return 1 + patch_status = detect_patch_status(repo_root, patch_path, apply_strategy=apply_strategy) if patch_status == "conflict": print( @@ -122,23 +139,19 @@ def main() -> int: return 0 -def find_repo_root(start: Path) -> Path: - candidate = start.resolve() - for current in (candidate, *candidate.parents): - if (current / ".github").is_dir(): - return current - raise FileNotFoundError(f"Unable to find repository root from {start}") - - def load_registry(path: Path) -> list[dict[str, str]]: payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {} overrides = payload.get("overrides") if not isinstance(overrides, list): raise ValueError("Registry must define an overrides list.") + REQUIRED_FIELDS = {"id", "patch_path", "target_path", "apply_strategy", "expected_content_hash"} normalized: list[dict[str, str]] = [] for item in overrides: if not isinstance(item, dict): raise ValueError("Each override entry must be a mapping.") + missing = REQUIRED_FIELDS - set(item.keys()) + if missing: + raise ValueError(f"Override entry missing required fields: {missing}") normalized.append({key: str(value) for key, value in item.items()}) return normalized @@ -153,13 +166,17 @@ def select_overrides( def run_git(command: list[str], repo_root: Path, quiet: bool = False) -> int: - completed = subprocess.run( - command, - cwd=repo_root, - text=True, - capture_output=True, - check=False, - ) + try: + completed = subprocess.run( + command, + cwd=repo_root, + text=True, + capture_output=True, + check=False, + timeout=60, + ) + except subprocess.TimeoutExpired: + return -1 if not quiet and completed.stdout.strip(): print(completed.stdout.strip()) if not quiet and completed.stderr.strip(): diff --git a/.github/skills/local-agent-sync-external-resources/scripts/check_external_refresh_workspace.py b/.github/skills/local-agent-sync-external-resources/scripts/check_external_refresh_workspace.py index c57b64fa..18a9acbe 100644 --- a/.github/skills/local-agent-sync-external-resources/scripts/check_external_refresh_workspace.py +++ b/.github/skills/local-agent-sync-external-resources/scripts/check_external_refresh_workspace.py @@ -62,7 +62,17 @@ def validate_workspace(root: Path, workspace: Path | None) -> list[str]: def find_repo_local_refresh_dirs(root: Path) -> list[str]: - return [relative for relative in REPO_LOCAL_REFRESH_DIRS if (root / relative).exists()] + found: list[str] = [] + tmp_dir = root / "tmp" + if tmp_dir.is_dir(): + for child in tmp_dir.iterdir(): + if child.is_dir() and (child / ".git").exists(): + found.append(child.relative_to(root).as_posix()) + for relative in REPO_LOCAL_REFRESH_DIRS: + path = root / relative + if path.exists() and path.as_posix() not in {f for f in found}: + found.append(relative) + return found def collect_findings( diff --git a/.github/skills/local-agent-sync-external-resources/scripts/normalize_superpowers_imports.py b/.github/skills/local-agent-sync-external-resources/scripts/normalize_superpowers_imports.py index 58ae7371..4fec0a47 100644 --- a/.github/skills/local-agent-sync-external-resources/scripts/normalize_superpowers_imports.py +++ b/.github/skills/local-agent-sync-external-resources/scripts/normalize_superpowers_imports.py @@ -17,6 +17,12 @@ import yaml +_SCRIPTS_LIB = Path(__file__).resolve().parents[3] / "scripts" / "lib" +if _SCRIPTS_LIB.parent.as_posix() not in sys.path: + sys.path.insert(0, _SCRIPTS_LIB.parent.as_posix()) + +from lib.repo_paths import find_repo_root + DEFAULT_REFERENCE = ( ".github/skills/local-agent-sync-external-resources/references/superpowers-normalization.yaml" @@ -51,6 +57,8 @@ class NormalizationConfig: managed_text_replacements: tuple[ManagedTextReplacement, ...] scan_includes: tuple[str, ...] ignored_files: frozenset[str] + blocked_local_prefixes: tuple[str, ...] = () + blocked_managed_reference_prefixes: tuple[str, ...] = () @dataclass(frozen=True) @@ -114,14 +122,6 @@ def main() -> int: return 0 -def find_repo_root(start: Path) -> Path: - candidate = start.resolve() - for current in (candidate, *candidate.parents): - if (current / ".github").is_dir(): - return current - raise FileNotFoundError(f"Unable to find repository root from {start}") - - def resolve_reference_path(repo_root: Path, raw_reference_path: Path) -> Path: if raw_reference_path.is_absolute(): return raw_reference_path @@ -144,6 +144,11 @@ def load_config(reference_path: Path) -> NormalizationConfig: if isinstance(raw_ignored_files, list): ignored_files.update(item for item in raw_ignored_files if isinstance(item, str) and item.strip()) + raw_blocked_local = payload.get("blocked_local_prefixes", []) + blocked_local_prefixes = tuple(item for item in raw_blocked_local if isinstance(item, str) and item.strip()) if isinstance(raw_blocked_local, list) else () + raw_blocked_managed = payload.get("blocked_managed_reference_prefix", []) + blocked_managed_reference_prefixes = tuple(item for item in raw_blocked_managed if isinstance(item, str) and item.strip()) if isinstance(raw_blocked_managed, list) else () + return NormalizationConfig( reference_path=reference_path, managed_skills=managed_skills, @@ -151,6 +156,8 @@ def load_config(reference_path: Path) -> NormalizationConfig: managed_text_replacements=managed_text_replacements, scan_includes=scan_includes, ignored_files=frozenset(ignored_files), + blocked_local_prefixes=blocked_local_prefixes, + blocked_managed_reference_prefixes=blocked_managed_reference_prefixes, ) @@ -228,6 +235,14 @@ def detect_drift(repo_root: Path, config: NormalizationConfig) -> list[Normaliza return changes +def _safe_replace(text: str, old: str, new: str) -> str: + import re + if old.startswith("superpowers:") or old.startswith("obra-"): + pattern = re.compile(r'(? list[NormalizationChange]: changes: list[NormalizationChange] = [] for old_relative_path, new_relative_path in path_renames(config): @@ -251,7 +266,7 @@ def apply_normalization(repo_root: Path, config: NormalizationConfig) -> list[No for old_text, new_text in replacements: if old_text not in updated_text: continue - updated_text = updated_text.replace(old_text, new_text) + updated_text = _safe_replace(updated_text, old_text, new_text) applied_replacements.append(f"{old_text} -> {new_text}") if updated_text == original_text: continue diff --git a/.github/skills/local-agent-sync-external-resources/scripts/sync_resource_fingerprint.py b/.github/skills/local-agent-sync-external-resources/scripts/sync_resource_fingerprint.py index 3b2a6db7..b34cb9dc 100644 --- a/.github/skills/local-agent-sync-external-resources/scripts/sync_resource_fingerprint.py +++ b/.github/skills/local-agent-sync-external-resources/scripts/sync_resource_fingerprint.py @@ -8,7 +8,13 @@ from pathlib import Path import sys -REPO_ROOT = Path(__file__).resolve().parents[4] +_SCRIPTS_LIB = Path(__file__).resolve().parents[3] / "scripts" / "lib" +if _SCRIPTS_LIB.parent.as_posix() not in sys.path: + sys.path.insert(0, _SCRIPTS_LIB.parent.as_posix()) + +from lib.repo_paths import find_repo_root + +REPO_ROOT = find_repo_root(Path(__file__).resolve()) SCRIPTS_ROOT = REPO_ROOT / ".github/scripts" DEFAULT_SNAPSHOT_OUTPUT = REPO_ROOT / "tmp/superpowers/internal-agent-sync-control-center.manifest.json" if SCRIPTS_ROOT.as_posix() not in sys.path: @@ -47,26 +53,40 @@ def main() -> int: def run_snapshot(args: argparse.Namespace) -> int: - root = Path(args.root).resolve() - files = collect_files(root, [Path(path) for path in args.paths]) - manifest = build_manifest(root, files, source_ref_base=args.source_ref_base) - output_path = Path(args.output) - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8") - print(output_path.as_posix()) - return 0 + try: + root = Path(args.root).resolve() + files = collect_files(root, [Path(path) for path in args.paths]) + manifest = build_manifest(root, files, source_ref_base=args.source_ref_base) + output_path = Path(args.output) + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print(output_path.as_posix()) + return 0 + except FileNotFoundError as exc: + print(f"[error] {exc}", file=sys.stderr) + return 1 + except json.JSONDecodeError as exc: + print(f"[error] manifest parse error: {exc}", file=sys.stderr) + return 1 def run_diff(args: argparse.Namespace) -> int: - old_manifest = load_manifest(Path(args.old)) - new_manifest = load_manifest(Path(args.new)) - result = diff_manifests(old_manifest, new_manifest) - if args.format == "json": - print(json.dumps(result, indent=2, sort_keys=True)) + try: + old_manifest = load_manifest(Path(args.old)) + new_manifest = load_manifest(Path(args.new)) + result = diff_manifests(old_manifest, new_manifest) + if args.format == "json": + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 + + print(render_diff_text(result)) return 0 - - print(render_diff_text(result)) - return 0 + except FileNotFoundError as exc: + print(f"[error] {exc}", file=sys.stderr) + return 1 + except json.JSONDecodeError as exc: + print(f"[error] manifest parse error: {exc}", file=sys.stderr) + return 1 if __name__ == "__main__": diff --git a/.github/skills/local-agent-sync-global-copilot-configs-into-repo/SKILL.md b/.github/skills/local-agent-sync-global-copilot-configs-into-repo/SKILL.md index 131aa052..57e66477 100644 --- a/.github/skills/local-agent-sync-global-copilot-configs-into-repo/SKILL.md +++ b/.github/skills/local-agent-sync-global-copilot-configs-into-repo/SKILL.md @@ -1,6 +1,6 @@ --- name: local-agent-sync-global-copilot-configs-into-repo -description: Use when aligning a consumer repository to this repository's managed GitHub Copilot baseline plus the explicitly shared repository-hygiene files and retained-learning ledger template, including mirror planning, apply runs, drift checks, and preservation of target `local-*` assets. +description: Use when aligning a consumer repository to this repository's managed GitHub Copilot baseline, shared repository-hygiene files, and retained-learning ledger template. --- # Internal Agent Sync Global Copilot Configs Into Repo @@ -21,7 +21,7 @@ The paired agent should not restate default mode handling, preserved `local-*` b - Align a consumer repository with the managed GitHub Copilot baseline from this repository. - Refresh target `AGENTS.md`, `.github/copilot-instructions.md`, and `.github/INVENTORY.md` to the current root-policy and review-only model after mirroring. -- Refresh shared repository-hygiene files that are part of the managed sync baseline, currently `.editorconfig`, `.pre-commit-config.yaml`, and `.github/workflows/_pre-commit.yml`. +- Refresh shared repository-hygiene files that are part of the managed sync baseline, currently `.editorconfig`, `.pre-commit-config.yaml`, `.github/workflows/_pre-commit.yml`, `.github/copilot-commit-message-instructions.md`, `.github/security-baseline.md`, `.github/DEPRECATION.md`, and `.github/repo-profiles.yml`. - Refresh repository-root `LESSONS_LEARNED.md` from the source structure while preserving and, when needed, migrating target-authored pending lesson rows. - Run or interpret `./.github/scripts/run.sh sync_copilot_catalog` or `.github/scripts/sync_copilot_catalog.py`. - Audit source-target drift before or after a sync. diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/agent_translation.py b/.github/skills/local-agent-sync-install-ai-resources/scripts/agent_translation.py index 9403d99f..d6ee9e54 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/agent_translation.py +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/agent_translation.py @@ -1,6 +1,6 @@ """Agent translation module for converting .agent.md to target-specific formats. -Translates Copilot-native .agent.md files into Claude, OpenCode, and Codex +Translates Copilot-native .agent.md files into OpenCode and Codex agent definitions while preserving the original body content and translating frontmatter fields appropriately. """ @@ -12,14 +12,6 @@ import yaml -_TOOL_MAP_CLAUDE = { - "read": "Read", - "edit": "Edit", - "search": "Glob, Grep", - "execute": "Bash", - "web": "WebSearch, WebFetch", -} - _PERMISSION_MAP_OPENCODE = { "read": "read", "edit": "edit", @@ -71,8 +63,6 @@ def translate_agent_for_target( if target == "copilot": return _translate_for_copilot(frontmatter, body) - elif target == "claude": - return _translate_for_claude(frontmatter, body) elif target == "opencode": return _translate_for_opencode(frontmatter, body) elif target == "codex": @@ -92,47 +82,6 @@ def _translate_for_copilot(frontmatter: dict, body: str) -> str: return render_frontmatter_md(frontmatter) + body -def _translate_for_claude(frontmatter: dict, body: str) -> str: - claude_fm: dict[str, object] = {} - if "name" in frontmatter: - claude_fm["name"] = frontmatter["name"] - if "description" in frontmatter: - claude_fm["description"] = frontmatter["description"] - - tools_list = _build_claude_tools(frontmatter) - if tools_list: - claude_fm["tools"] = tools_list - - output = render_frontmatter_md(claude_fm) - output += body.rstrip() + "\n" - - handoffs_body = _render_handoffs_body(frontmatter) - if handoffs_body: - output += "\n" + handoffs_body + "\n" - - return output - - -def _build_claude_tools(frontmatter: dict) -> str: - parts: list[str] = [] - - agents = frontmatter.get("agents") or [] - if isinstance(agents, list) and agents: - agent_names = ", ".join(agents) - parts.append(f"Agent({agent_names})") - - tools = frontmatter.get("tools") or [] - if isinstance(tools, list): - for tool in tools: - mapped = _TOOL_MAP_CLAUDE.get(tool) - if mapped: - for t in mapped.split(", "): - if t not in parts: - parts.append(t) - - return ", ".join(parts) - - def _render_handoffs_body(frontmatter: dict) -> str: handoffs = frontmatter.get("handoffs") or [] if not isinstance(handoffs, list) or not handoffs: diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/bisync_skills.py b/.github/skills/local-agent-sync-install-ai-resources/scripts/bisync_skills.py index e5e2bd3b..ccd669e0 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/bisync_skills.py +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/bisync_skills.py @@ -16,40 +16,33 @@ import json import shutil import subprocess +import sys from dataclasses import dataclass, field from pathlib import Path +_SCRIPTS_LIB = Path(__file__).resolve().parents[3] / "scripts" / "lib" +if _SCRIPTS_LIB.parent.as_posix() not in sys.path: + sys.path.insert(0, _SCRIPTS_LIB.parent.as_posix()) + from home_sync_contract import load_home_sync_policy, state_root_for_home from home_syncing import reconcile_manifest_entry_after_bisync_copy +from lib.sync_exclusions import should_ignore_sync_path, sync_copytree_ignore from sync_output import ( build_compact_bisync_output, dump_compact_json, render_bisync_report, ) -IGNORED_SYNC_PARTS: tuple[str, ...] = (".venv", "__pycache__", ".pytest_cache") -IGNORED_SYNC_SUFFIXES: tuple[str, ...] = (".pyc", ".pyo") EXCLUDED_BUNDLE_PREFIX: str = "local-" BISYNC_PLAN_PATH = "last-bisync-plan.json" def should_ignore(path: Path) -> bool: - return ( - any(part in IGNORED_SYNC_PARTS for part in path.parts) - or path.suffix in IGNORED_SYNC_SUFFIXES - ) + return should_ignore_sync_path(path) def should_ignore_copytree(directory: str, names: list[str]) -> set[str]: - base = Path(directory) - ignored: set[str] = set() - for name in names: - candidate = base / name - if candidate.is_dir() and name in IGNORED_SYNC_PARTS: - ignored.add(name) - elif candidate.is_file() and candidate.suffix in IGNORED_SYNC_SUFFIXES: - ignored.add(name) - return ignored + return sync_copytree_ignore(directory, names) def get_max_mtime(directory: Path) -> float: @@ -71,13 +64,17 @@ def hash_bundle(directory: Path) -> str: def is_repo_clean(source_root: Path) -> tuple[bool, str, str]: - result = subprocess.run( - ["git", "status", "--porcelain", "--untracked-files=all"], - cwd=source_root, - text=True, - capture_output=True, - check=False, - ) + try: + result = subprocess.run( + ["git", "status", "--porcelain", "--untracked-files=all"], + cwd=source_root, + text=True, + capture_output=True, + check=False, + timeout=10, + ) + except subprocess.TimeoutExpired: + return False, "bisync-repo-git-failed", "git status timed out" if result.returncode != 0: return False, "bisync-repo-git-failed", "Unable to run git status before bisync apply." if result.stdout.strip(): @@ -145,7 +142,7 @@ def compute_next_action( } -@dataclass +@dataclass(frozen=True) class BisyncDriftEntry: skill_name: str drift_type: str @@ -156,7 +153,7 @@ class BisyncDriftEntry: home_hash: str = "" repo_mtime: float = 0.0 home_mtime: float = 0.0 - blocked_codes: list[str] = field(default_factory=list) + blocked_codes: tuple[str, ...] = () def to_dict(self) -> dict: result: dict = { @@ -424,6 +421,18 @@ def apply_bisync_plan( else: continue + for check_path in (src, dst): + resolved = check_path.resolve() + if check_path.is_symlink() or any(p.is_symlink() for p in check_path.parents): + plan.blocked_codes.append("symlink-not-allowed") + plan.blocked_codes = sorted(set(plan.blocked_codes)) + plan.verification = {"status": "blocked", "code": "symlink-not-allowed", "path": str(check_path)} + plan.next_step = _next_step_for_bisync(plan) + plan.next_action = compute_next_action( + plan.blocked_codes, bool(plan.drifts), "apply", source_root, home_root + ) + return plan + if dst.exists(): shutil.rmtree(dst) shutil.copytree(src, dst, ignore=should_ignore_copytree) diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/home_sync_contract.py b/.github/skills/local-agent-sync-install-ai-resources/scripts/home_sync_contract.py index 6b8c7588..2e2c8b86 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/home_sync_contract.py +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/home_sync_contract.py @@ -59,22 +59,31 @@ class HomeSyncPolicy: def load_runtime_support_matrix(source_root: Path) -> list[RuntimeSupportRow]: matrix_path = resolve_skill_reference(source_root, RUNTIME_SUPPORT_MATRIX_PATH) - payload = yaml.safe_load(matrix_path.read_text(encoding="utf-8")) or {} + try: + payload = yaml.safe_load(matrix_path.read_text(encoding="utf-8")) or {} + except yaml.YAMLError as exc: + raise ValueError(f"manifest-corrupt: failed to parse runtime support matrix: {exc}") from exc rows = payload.get("rows", []) - return [ - RuntimeSupportRow( - target=row["target"], - resource_family=row["resource_family"], - support_level=row["support_level"], - home_path=row.get("home_path"), - direct_copy_possible=bool(row.get("direct_copy_possible")), - translation_required=bool(row.get("translation_required")), - include_in_v1=bool(row.get("include_in_v1")), - evidence=tuple(row.get("evidence", [])), - notes=row.get("notes", ""), + result = [] + for row in rows: + evidence_raw = row.get("evidence", []) + if isinstance(evidence_raw, str): + raise ValueError("manifest-corrupt: evidence must be a list, got string") + evidence = tuple(evidence_raw) if isinstance(evidence_raw, list) else () + result.append( + RuntimeSupportRow( + target=row["target"], + resource_family=row["resource_family"], + support_level=row["support_level"], + home_path=row.get("home_path"), + direct_copy_possible=bool(row.get("direct_copy_possible")), + translation_required=bool(row.get("translation_required")), + include_in_v1=bool(row.get("include_in_v1")), + evidence=evidence, + notes=row.get("notes", ""), + ) ) - for row in rows - ] + return result def load_home_sync_catalog(source_root: Path) -> list[CatalogResource]: @@ -141,7 +150,10 @@ def load_home_sync_policy(source_root: Path) -> HomeSyncPolicy: def _load_home_sync_catalog_payload(source_root: Path) -> dict[str, object]: catalog_path = resolve_skill_reference(source_root, HOME_SYNC_CATALOG_PATH) - return yaml.safe_load(catalog_path.read_text(encoding="utf-8")) or {} + try: + return yaml.safe_load(catalog_path.read_text(encoding="utf-8")) or {} + except yaml.YAMLError as exc: + raise ValueError(f"manifest-corrupt: failed to parse home sync catalog: {exc}") from exc def discover_skill_resources( @@ -207,11 +219,6 @@ def has_agent_root(target: str) -> bool: return target in TARGET_AGENT_ROOTS -def load_agent_catalog(source_root: Path) -> list[CatalogResource]: - catalog = load_home_sync_catalog(source_root) - return [resource for resource in catalog if resource.source_family == "agents"] - - def resolve_support_row( rows: list[RuntimeSupportRow], target: str, resource_family: str ) -> RuntimeSupportRow | None: diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/home_syncing.py b/.github/skills/local-agent-sync-install-ai-resources/scripts/home_syncing.py index a2d650e6..3be09d77 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/home_syncing.py +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/home_syncing.py @@ -5,10 +5,15 @@ import os import shutil import subprocess +import sys from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path +_SCRIPTS_LIB = Path(__file__).resolve().parents[3] / "scripts" / "lib" +if _SCRIPTS_LIB.parent.as_posix() not in sys.path: + sys.path.insert(0, _SCRIPTS_LIB.parent.as_posix()) + from agent_translation import target_extension, translate_agent_for_target from home_sync_contract import ( TARGET_ORDER, @@ -24,6 +29,12 @@ runtime_skill_root, state_root_for_home, ) +from lib.sync_exclusions import ( + IGNORED_SYNC_PARTS, + IGNORED_SYNC_SUFFIXES, + should_ignore_sync_path as _shared_should_ignore_sync_path, + sync_copytree_ignore, +) MANIFEST_PATH = "manifest.json" LAST_PLAN_PATH = "last-plan.json" @@ -31,8 +42,6 @@ LOCK_PATH = "locks/home-ai-resources.lock" NORMALIZATION_VERSION = "v1" TEXT_EXTENSIONS = (".md", ".txt", ".yml", ".yaml", ".json", ".sh", ".py") -IGNORED_SYNC_PARTS = {".venv", "__pycache__", ".pytest_cache"} -IGNORED_SYNC_SUFFIXES = {".pyc", ".pyo"} @dataclass(frozen=True) @@ -179,7 +188,7 @@ def build_home_sync_plan( source_root = source_root.resolve() home_root = home_root.resolve() state_root = state_root_for_home(home_root) - if is_relative_to(source_root, state_root): + if source_root.is_relative_to(state_root): raise RuntimeError("reverse-sync-blocked: source_root is under the home sync state, sync must be repo → home only") runtime_rows = load_runtime_support_matrix(source_root) catalog = load_home_sync_catalog(source_root) @@ -319,6 +328,18 @@ def build_home_sync_plan( ) +def _verify_apply_confinement(target_path: Path, home_root: Path) -> str | None: + resolved_home = home_root.resolve() + if target_path.is_symlink() or any(p.is_symlink() for p in target_path.parents if p != target_path): + return "symlink-not-allowed" + try: + resolved_target = target_path.resolve() if target_path.exists() else target_path + resolved_target.relative_to(resolved_home) + except ValueError: + return "unsafe-home-path" + return None + + def apply_home_sync_plan( plan: HomeSyncPlan, *, @@ -345,6 +366,9 @@ def apply_home_sync_plan( if operation.action not in {"copy", "delete"}: continue target_path = Path(operation.path) + confinement_code = _verify_apply_confinement(target_path, plan.home_root) + if confinement_code: + raise RuntimeError(f"{confinement_code}: {target_path}") if operation.action == "delete": if prune_managed: remove_resource(target_path) @@ -891,7 +915,7 @@ def _stale_confinement_check( stale_path = home_root / target_path resolved_stale = stale_path.resolve() - if not is_relative_to(resolved_stale, resolved_home): + if not resolved_stale.is_relative_to(resolved_home): if stale_path.is_symlink() or any(p.is_symlink() for p in stale_path.parents): return "symlink-not-allowed" return "unsafe-home-path" @@ -907,7 +931,7 @@ def _stale_confinement_check( else: return "unsafe-home-path" - if not is_relative_to(resolved_stale, expected_root): + if not resolved_stale.is_relative_to(expected_root): return "unsafe-home-path" if stale_path.is_symlink() or any(p.is_symlink() for p in stale_path.parents): @@ -1254,7 +1278,7 @@ def assess_target_root_safety( ) -> HomeSyncOperation | None: resolved_home = home_root.resolve() resolved_target = target_root.resolve() - if not is_relative_to(resolved_target, resolved_home): + if not resolved_target.is_relative_to(resolved_home): code = "symlink-not-allowed" if target_root.is_symlink() else "unsafe-home-path" return HomeSyncOperation( target=target, @@ -1295,17 +1319,11 @@ def remove_resource(target_path: Path) -> None: def copytree_ignore_runtime_artifacts(directory: str, names: list[str]) -> set[str]: - return { - name - for name in names - if should_ignore_sync_path(Path(directory, name)) - } + return sync_copytree_ignore(directory, names) def should_ignore_sync_path(path: Path) -> bool: - if any(part in IGNORED_SYNC_PARTS for part in path.parts): - return True - return path.suffix in IGNORED_SYNC_SUFFIXES + return _shared_should_ignore_sync_path(path) def resource_mtime(path: Path) -> float: @@ -1319,14 +1337,6 @@ def resource_mtime(path: Path) -> float: return max_time -def is_relative_to(path: Path, other: Path) -> bool: - try: - path.relative_to(other) - except ValueError: - return False - return True - - def is_read_write_accessible(path: Path) -> bool: return path.exists() and path.is_dir() and os.access(path, os.R_OK | os.W_OK | os.X_OK) @@ -1430,16 +1440,20 @@ def next_action_for_plan( def git_revision(root: Path) -> str | None: - result = subprocess.run( - ["git", "rev-parse", "--short", "HEAD"], - cwd=root, - text=True, - capture_output=True, - check=False, - ) - if result.returncode != 0: + try: + result = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + cwd=root, + text=True, + capture_output=True, + check=False, + timeout=10, + ) + if result.returncode != 0: + return None + return result.stdout.strip() or None + except (subprocess.TimeoutExpired, OSError): return None - return result.stdout.strip() or None def render_json(data: object) -> str: diff --git a/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_home_ai_resources.py b/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_home_ai_resources.py index 3525e26b..2c349c31 100644 --- a/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_home_ai_resources.py +++ b/.github/skills/local-agent-sync-install-ai-resources/scripts/sync_home_ai_resources.py @@ -41,8 +41,7 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( description="Plan, audit, doctor, or apply allowlisted home AI resource sync operations." ) - subparsers = parser.add_subparsers(dest="command", required=False) - subparsers.required = True + subparsers = parser.add_subparsers(dest="command", required=True) for cmd in ("sync", "plan", "apply", "audit", "doctor", "dry-run"): cmd_parser = subparsers.add_parser(cmd, help=f"Run {cmd} sync operation.") @@ -222,17 +221,30 @@ def run(args: argparse.Namespace) -> int: emit_output(payload, format_name=args.format) return 0 - plan = build_home_sync_plan( - source_root=source_root, - home_root=home_root, - targets=targets, - retired_targets=retire_targets, - mode=command, - experimental_targets=args.experimental_targets, - prune_managed=args.prune_managed, - fast=args.fast, - changed_only=args.changed_only, - ) + try: + plan = build_home_sync_plan( + source_root=source_root, + home_root=home_root, + targets=targets, + retired_targets=retire_targets, + mode=command, + experimental_targets=args.experimental_targets, + prune_managed=args.prune_managed, + fast=args.fast, + changed_only=args.changed_only, + ) + except (RuntimeError, ValueError, FileNotFoundError, KeyError) as exc: + code = _extract_blocked_code(exc) + payload = { + "mode": command, + "selected_targets": list(targets), + "retired_targets": list(retire_targets), + "blocked_codes": [code], + "error": str(exc), + **blocked_report_fields(str(exc)), + } + emit_output(payload, format_name=args.format, failure_message=str(exc)) + return 1 if command == "apply": return run_apply(plan, args) @@ -251,17 +263,30 @@ def run_sync( retire_targets: tuple[str, ...], args: argparse.Namespace, ) -> int: - install_plan = build_home_sync_plan( - source_root=source_root, - home_root=home_root, - targets=targets, - retired_targets=retire_targets, - mode="apply", - experimental_targets=args.experimental_targets, - prune_managed=args.prune_managed, - fast=args.fast, - changed_only=args.changed_only, - ) + try: + install_plan = build_home_sync_plan( + source_root=source_root, + home_root=home_root, + targets=targets, + retired_targets=retire_targets, + mode="apply", + experimental_targets=args.experimental_targets, + prune_managed=args.prune_managed, + fast=args.fast, + changed_only=args.changed_only, + ) + except (RuntimeError, ValueError, FileNotFoundError, KeyError) as exc: + code = _extract_blocked_code(exc) + payload = { + "mode": "sync", + "status": "blocked", + "blocked_codes": [code], + "reason": str(exc), + "install": {"error": str(exc)}, + "bisync": None, + } + emit_sync_output(payload, format_name=args.format) + return 1 install_payload = install_plan.to_dict() auto_blockers = install_auto_apply_blockers(install_plan, args) if auto_blockers: @@ -454,6 +479,13 @@ def blocked_report_fields(reason: str) -> dict[str, object]: } +def _extract_blocked_code(exc: BaseException) -> str: + message = str(exc) + if ":" in message: + return message.split(":", 1)[0].strip() + return type(exc).__name__ + + def normalize_mode(command: str) -> str: return "plan" if command == "dry-run" else command diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_apply_overrides.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_apply_overrides.py new file mode 100644 index 00000000..e5c2cc3f --- /dev/null +++ b/tests/github/skills/local-agent-sync-external-resources/scripts/test_apply_overrides.py @@ -0,0 +1,80 @@ +import subprocess +import sys +from pathlib import Path +from unittest.mock import patch + +import pytest + +REPO_ROOT = next( + parent for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +SCRIPT_DIR = ( + REPO_ROOT / ".github/skills/local-agent-sync-external-resources/scripts" +) +sys.path.insert(0, SCRIPT_DIR.as_posix()) + +from apply_imported_asset_overrides import ( # noqa: E402 + load_registry, + select_overrides, + detect_patch_status, +) + + +def test_load_registry_validates_required_fields(tmp_path: Path) -> None: + registry = tmp_path / "registry.yaml" + registry.write_text( + "overrides:\n" + " - id: test\n" + " patch_path: patches/test.patch\n" + " target_path: .github/skills/test/SKILL.md\n" + " apply_strategy: git-apply\n" + " expected_content_hash: abc123\n", + encoding="utf-8", + ) + result = load_registry(registry) + assert len(result) == 1 + assert result[0]["id"] == "test" + + +def test_load_registry_rejects_non_list_overrides(tmp_path: Path) -> None: + registry = tmp_path / "registry.yaml" + registry.write_text("overrides: not-a-list\n", encoding="utf-8") + with pytest.raises(ValueError, match="overrides list"): + load_registry(registry) + + +def test_load_registry_rejects_non_mapping_entries(tmp_path: Path) -> None: + registry = tmp_path / "registry.yaml" + registry.write_text("overrides:\n - just-a-string\n", encoding="utf-8") + with pytest.raises(ValueError, match="mapping"): + load_registry(registry) + + +def test_select_overrides_filters_by_id() -> None: + overrides = [ + {"id": "a", "patch_path": "a.patch"}, + {"id": "b", "patch_path": "b.patch"}, + ] + assert len(select_overrides(overrides, ["a"])) == 1 + assert select_overrides(overrides, None) == overrides + + +def test_detect_patch_status_returns_conflict_for_bad_patch(tmp_path: Path) -> None: + repo_root = tmp_path / "repo" + repo_root.mkdir() + subprocess.run(["git", "init", "-q"], cwd=repo_root, check=True, timeout=10) + subprocess.run(["git", "config", "user.email", "t@t.com"], cwd=repo_root, check=True, timeout=10) + subprocess.run(["git", "config", "user.name", "t"], cwd=repo_root, check=True, timeout=10) + (repo_root / "file.txt").write_text("content\n", encoding="utf-8") + subprocess.run(["git", "add", "."], cwd=repo_root, check=True, timeout=10) + subprocess.run(["git", "commit", "-qm", "init"], cwd=repo_root, check=True, timeout=10) + + bad_patch = tmp_path / "bad.patch" + bad_patch.write_text( + "--- a/nonexistent.txt\n+++ b/nonexistent.txt\n@@ -1 +1 @@\n-old\n+new\n", + encoding="utf-8", + ) + + status = detect_patch_status(repo_root, bad_patch) + assert status == "conflict" diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_normalize.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_normalize.py new file mode 100644 index 00000000..2ed5a60e --- /dev/null +++ b/tests/github/skills/local-agent-sync-external-resources/scripts/test_normalize.py @@ -0,0 +1,54 @@ +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = next( + parent for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +SCRIPT_DIR = ( + REPO_ROOT / ".github/skills/local-agent-sync-external-resources/scripts" +) +sys.path.insert(0, SCRIPT_DIR.as_posix()) + +from normalize_superpowers_imports import ( # noqa: E402 + load_config, + detect_drift, + NormalizationConfig, + ManagedSkill, +) + + +def test_load_config_validates_managed_skills(tmp_path: Path) -> None: + ref = tmp_path / "ref.yaml" + ref.write_text("managed_skills: not-a-list\n", encoding="utf-8") + with pytest.raises(ValueError, match="managed_skills"): + load_config(ref) + + +def test_detect_drift_finds_legacy_path(tmp_path: Path) -> None: + repo_root = tmp_path / "repo" + repo_root.mkdir() + (repo_root / ".github" / "skills" / "obra-brainstorming").mkdir(parents=True) + (repo_root / ".github" / "skills" / "obra-brainstorming" / "SKILL.md").write_text( + "# old\n", encoding="utf-8" + ) + ref_dir = repo_root / ".github" / "skills" / "local-agent-sync-external-resources" / "references" + ref_dir.mkdir(parents=True) + ref = ref_dir / "ref.yaml" + ref.write_text("managed_skills: []\n", encoding="utf-8") + config = NormalizationConfig( + reference_path=ref, + managed_skills=(ManagedSkill( + upstream="brainstorming", + legacy_local="obra-brainstorming", + local="superpowers-brainstorming", + ),), + managed_patches=(), + managed_text_replacements=(), + scan_includes=(".github/skills",), + ignored_files=frozenset(), + ) + changes = detect_drift(repo_root, config) + assert any(c.kind == "legacy-path" for c in changes) diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_workspace_check.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_workspace_check.py new file mode 100644 index 00000000..0396d1e1 --- /dev/null +++ b/tests/github/skills/local-agent-sync-external-resources/scripts/test_workspace_check.py @@ -0,0 +1,51 @@ +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = next( + parent for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +SCRIPT_DIR = ( + REPO_ROOT / ".github/skills/local-agent-sync-external-resources/scripts" +) +sys.path.insert(0, SCRIPT_DIR.as_posix()) + +from check_external_refresh_workspace import ( # noqa: E402 + validate_workspace, + find_repo_local_refresh_dirs, +) + + +def test_validate_workspace_blocks_inside_repo(tmp_path: Path) -> None: + workspace = tmp_path / "subdir" + workspace.mkdir() + findings = validate_workspace(tmp_path, workspace) + assert len(findings) == 1 + assert "outside the repository" in findings[0] + + +def test_validate_workspace_allows_outside_repo(tmp_path: Path) -> None: + workspace = tmp_path / "external-workspace" + workspace.mkdir() + repo = tmp_path / "repo" + repo.mkdir() + findings = validate_workspace(repo, workspace) + assert findings == [] + + +def test_validate_workspace_no_check_when_none(tmp_path: Path) -> None: + findings = validate_workspace(tmp_path, None) + assert findings == [] + + +def test_find_repo_local_refresh_dirs_detects_leftovers(tmp_path: Path) -> None: + (tmp_path / "tmp" / "external-refresh").mkdir(parents=True) + dirs = find_repo_local_refresh_dirs(tmp_path) + assert "tmp/external-refresh" in dirs + + +def test_find_repo_local_refresh_dirs_empty_when_clean(tmp_path: Path) -> None: + dirs = find_repo_local_refresh_dirs(tmp_path) + assert dirs == [] diff --git a/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_apply_paths.py b/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_apply_paths.py new file mode 100644 index 00000000..3f6d94eb --- /dev/null +++ b/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_apply_paths.py @@ -0,0 +1,160 @@ +import json +import sys +import textwrap +from pathlib import Path + +import pytest + +REPO_ROOT = next( + parent for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +SCRIPT_DIR = ( + REPO_ROOT / ".github/skills/local-agent-sync-install-ai-resources/scripts" +) +sys.path.insert(0, SCRIPT_DIR.as_posix()) + +from home_syncing import ( # noqa: E402 + HomeSyncOperation, + HomeSyncPlan, + ManagedResource, + apply_home_sync_plan, + hash_resource, + _stale_confinement_check, +) + + +def _setup_skill_source(tmp_path: Path) -> tuple[Path, str]: + skill_dir = tmp_path / ".github" / "skills" / "demo" + skill_dir.mkdir(parents=True) + (skill_dir / "SKILL.md").write_text("# demo\ncontent\n", encoding="utf-8") + content_hash = hash_resource(skill_dir) + return skill_dir, content_hash + + +def test_apply_copies_skill_to_home(tmp_path: Path) -> None: + _, content_hash = _setup_skill_source(tmp_path) + home_root = tmp_path / "home" + target_path = home_root / ".agents" / "skills" / "demo" + target_path.parent.mkdir(parents=True) + state_root = tmp_path / "state" + state_root.mkdir() + + resource = ManagedResource( + target="skills", resource_id="demo", resource_family="skills", + source_path=".github/skills/demo", target_path=str(target_path), + source_hash="abc", content_hash=content_hash, last_action="copy", + ) + operation = HomeSyncOperation( + target="skills", action="copy", path=str(target_path), + reason="first install", source_path=".github/skills/demo", resource_id="demo", + ) + plan = HomeSyncPlan( + source_root=tmp_path, home_root=home_root, state_root=state_root, + mode="apply", selected_targets=("skills",), retired_targets=(), + source_revision="abc", source_resources_considered=1, + operations=(operation,), desired_resources=(resource,), + missing_dirs=(), unsupported_families_by_target={}, residual_drift=(), + ) + + manifest_path = apply_home_sync_plan(plan) + assert manifest_path.is_file() + assert (target_path / "SKILL.md").is_file() + assert hash_resource(target_path) == content_hash + + +def test_apply_delete_with_prune(tmp_path: Path) -> None: + home_root = tmp_path / "home" + stale_path = home_root / ".agents" / "skills" / "old-skill" + stale_path.mkdir(parents=True) + (stale_path / "SKILL.md").write_text("# old\n", encoding="utf-8") + state_root = tmp_path / "state" + state_root.mkdir() + + operation = HomeSyncOperation( + target="skills", action="delete", path=str(stale_path), + reason="stale managed resource", code="stale-managed", + ) + plan = HomeSyncPlan( + source_root=tmp_path, home_root=home_root, state_root=state_root, + mode="apply", selected_targets=("skills",), retired_targets=(), + source_revision="abc", source_resources_considered=0, + operations=(operation,), desired_resources=(), + missing_dirs=(), unsupported_families_by_target={}, residual_drift=(), + ) + + apply_home_sync_plan(plan, prune_managed=True) + assert not stale_path.exists() + + +def test_apply_delete_without_prune_preserves_file(tmp_path: Path) -> None: + home_root = tmp_path / "home" + stale_path = home_root / ".agents" / "skills" / "old-skill" + stale_path.mkdir(parents=True) + (stale_path / "SKILL.md").write_text("# old\n", encoding="utf-8") + state_root = tmp_path / "state" + state_root.mkdir() + + operation = HomeSyncOperation( + target="skills", action="delete", path=str(stale_path), + reason="stale managed resource", code="stale-managed", + ) + plan = HomeSyncPlan( + source_root=tmp_path, home_root=home_root, state_root=state_root, + mode="apply", selected_targets=("skills",), retired_targets=(), + source_revision="abc", source_resources_considered=0, + operations=(operation,), desired_resources=(), + missing_dirs=(), unsupported_families_by_target={}, residual_drift=(), + ) + + apply_home_sync_plan(plan, prune_managed=False) + assert stale_path.exists() + + +def test_stale_confinement_blocks_path_outside_home(tmp_path: Path) -> None: + home_root = tmp_path / "home" + home_root.mkdir() + outside_path = tmp_path / "outside" / "skill" + outside_path.mkdir(parents=True) + + code = _stale_confinement_check( + item={"target": "skills", "resource_family": "skills", + "resource_id": "x", "content_hash": "abc"}, + target_path=str(outside_path), + home_root=home_root, + mode="plan", + ) + assert code == "unsafe-home-path" + + +def test_stale_confinement_blocks_symlink(tmp_path: Path) -> None: + home_root = tmp_path / "home" + home_root.mkdir() + target_root = home_root / ".agents" / "skills" + target_root.mkdir(parents=True) + outside = tmp_path / "outside-target" + outside.mkdir() + symlink = target_root / "linked-skill" + symlink.symlink_to(outside) + + code = _stale_confinement_check( + item={"target": "skills", "resource_family": "skills", + "resource_id": "x", "content_hash": "abc"}, + target_path=str(symlink), + home_root=home_root, + mode="plan", + ) + assert code == "symlink-not-allowed" + + +def test_stale_confinement_detects_corrupt_manifest_entry(tmp_path: Path) -> None: + home_root = tmp_path / "home" + home_root.mkdir() + + code = _stale_confinement_check( + item={"target": "skills"}, + target_path=str(home_root / ".agents" / "skills" / "x"), + home_root=home_root, + mode="plan", + ) + assert code == "manifest-corrupt" diff --git a/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_apply_safety.py b/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_apply_safety.py new file mode 100644 index 00000000..65234071 --- /dev/null +++ b/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_apply_safety.py @@ -0,0 +1,88 @@ +import json +import sys +import textwrap +from pathlib import Path + +import pytest + +REPO_ROOT = next( + parent for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +SCRIPT_DIR = ( + REPO_ROOT / ".github/skills/local-agent-sync-install-ai-resources/scripts" +) +sys.path.insert(0, SCRIPT_DIR.as_posix()) + +from home_syncing import ( # noqa: E402 + HomeSyncOperation, + HomeSyncPlan, + ManagedResource, + apply_home_sync_plan, +) + + +def _make_plan(tmp_path: Path, target_path: Path) -> HomeSyncPlan: + source_skill = tmp_path / ".github" / "skills" / "demo" + source_skill.mkdir(parents=True) + (source_skill / "SKILL.md").write_text("# demo\n", encoding="utf-8") + + resource = ManagedResource( + target="skills", + resource_id="demo", + resource_family="skills", + source_path=".github/skills/demo", + target_path=str(target_path), + source_hash="abc", + content_hash="abc", + last_action="copy", + ) + operation = HomeSyncOperation( + target="skills", + action="copy", + path=str(target_path), + reason="first install", + code=None, + source_path=".github/skills/demo", + resource_id="demo", + ) + return HomeSyncPlan( + source_root=tmp_path, + home_root=tmp_path / "home", + state_root=tmp_path / "state", + mode="apply", + selected_targets=("skills",), + retired_targets=(), + source_revision="abc1234", + source_resources_considered=1, + operations=(operation,), + desired_resources=(resource,), + missing_dirs=(), + unsupported_families_by_target={}, + residual_drift=(), + ) + + +def test_apply_blocks_path_escaping_home_root(tmp_path: Path) -> None: + escape_path = tmp_path / "outside" / "skills" / "demo" + plan = _make_plan(tmp_path, escape_path) + (tmp_path / "state").mkdir() + + with pytest.raises(RuntimeError, match="unsafe-home-path"): + apply_home_sync_plan(plan) + + +def test_apply_blocks_symlink_escape(tmp_path: Path) -> None: + home = tmp_path / "home" + home.mkdir() + outside = tmp_path / "outside-target" + outside.mkdir() + symlink_path = home / ".agents" / "skills" / "demo" + symlink_path.parent.mkdir(parents=True, exist_ok=True) + symlink_path.symlink_to(outside) + + plan = _make_plan(tmp_path, symlink_path) + (tmp_path / "state").mkdir() + + with pytest.raises(RuntimeError, match="symlink-not-allowed|unsafe-home-path"): + apply_home_sync_plan(plan) diff --git a/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_error_handling.py b/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_error_handling.py new file mode 100644 index 00000000..b4892330 --- /dev/null +++ b/tests/github/skills/local-agent-sync-install-ai-resources/scripts/test_error_handling.py @@ -0,0 +1,86 @@ +import json +import sys +import textwrap +from pathlib import Path + +import pytest + +REPO_ROOT = next( + parent for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +SCRIPT_DIR = ( + REPO_ROOT / ".github/skills/local-agent-sync-install-ai-resources/scripts" +) +sys.path.insert(0, SCRIPT_DIR.as_posix()) + +from home_sync_contract import load_home_sync_catalog # noqa: E402 +from home_syncing import build_home_sync_plan # noqa: E402 + + +def test_load_catalog_raises_valueerror_on_malformed_yaml(tmp_path: Path) -> None: + refs_dir = ( + tmp_path / ".github" / "skills" + / "local-agent-sync-install-ai-resources" / "references" + ) + refs_dir.mkdir(parents=True) + (refs_dir / "home-sync-catalog.yaml").write_text( + "not: a: valid: yaml: [", encoding="utf-8" + ) + with pytest.raises((ValueError, Exception)): + load_home_sync_catalog(tmp_path) + + +def test_load_catalog_raises_valueerror_on_missing_keys(tmp_path: Path) -> None: + refs_dir = ( + tmp_path / ".github" / "skills" + / "local-agent-sync-install-ai-resources" / "references" + ) + refs_dir.mkdir(parents=True) + (refs_dir / "home-sync-catalog.yaml").write_text( + textwrap.dedent("""\ + version: 1 + defaults: + include_internal_skills: true + resources: + - source_path: foo + """), + encoding="utf-8", + ) + with pytest.raises((ValueError, KeyError)): + load_home_sync_catalog(tmp_path) + + +def test_build_plan_raises_reverse_sync_blocked(tmp_path: Path) -> None: + refs_dir = ( + tmp_path / ".github" / "skills" + / "local-agent-sync-install-ai-resources" / "references" + ) + refs_dir.mkdir(parents=True) + (refs_dir / "home-sync-catalog.yaml").write_text( + textwrap.dedent("""\ + version: 1 + defaults: + include_internal_skills: true + include_local_skills: false + include_unlisted_skills: true + unmanaged_existing_skills_policy: repo-wins + excluded_skills: [] + skill_targets: + - codex + resources: [] + """), + encoding="utf-8", + ) + home_root = tmp_path / "home" + home_root.mkdir() + state_root = home_root / ".sync" / "cloud-strategy-governance" / "home-ai-resources" + state_root.mkdir(parents=True) + source_under_state = state_root / "fake-source" + source_under_state.mkdir() + (source_under_state / ".github").mkdir() + + with pytest.raises(RuntimeError, match="reverse-sync-blocked"): + build_home_sync_plan( + source_under_state, home_root, ("skills",), mode="plan" + ) From 41bcede3a3d99c0906d1bc391ac4a83926848ee3 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Wed, 8 Jul 2026 15:29:08 +0200 Subject: [PATCH 26/59] feat: add repository root discovery and sync exclusion rules with corresponding tests --- .github/INVENTORY.md | 8 ++- .github/scripts/lib/repo_paths.py | 13 ++++ .github/scripts/lib/shared.py | 60 +++++++++++-------- .github/scripts/lib/sync_exclusions.py | 34 +++++++++++ tests/github/scripts/lib/test_repo_paths.py | 23 +++++++ .../scripts/lib/test_subprocess_timeout.py | 32 ++++++++++ .../scripts/lib/test_sync_exclusions.py | 38 ++++++++++++ 7 files changed, 181 insertions(+), 27 deletions(-) create mode 100644 .github/scripts/lib/repo_paths.py create mode 100644 .github/scripts/lib/sync_exclusions.py create mode 100644 tests/github/scripts/lib/test_repo_paths.py create mode 100644 tests/github/scripts/lib/test_subprocess_timeout.py create mode 100644 tests/github/scripts/lib/test_sync_exclusions.py diff --git a/.github/INVENTORY.md b/.github/INVENTORY.md index 3e4cbc7a..9e458025 100644 --- a/.github/INVENTORY.md +++ b/.github/INVENTORY.md @@ -73,7 +73,6 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/internal-bash/SKILL.md` - `.github/skills/internal-changelog-automation/SKILL.md` - `.github/skills/internal-cloud-policy/SKILL.md` -- `.github/skills/internal-review-code/SKILL.md` - `.github/skills/internal-context-handoff/SKILL.md` - `.github/skills/internal-copilot-audit/SKILL.md` - `.github/skills/internal-copilot-docs-research/SKILL.md` @@ -98,7 +97,6 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/internal-github-pr/SKILL.md` - `.github/skills/internal-github-strategic/SKILL.md` - `.github/skills/internal-go/SKILL.md` -- `.github/skills/internal-review-high-level/SKILL.md` - `.github/skills/internal-java-project/SKILL.md` - `.github/skills/internal-java-spring-boot-development/SKILL.md` - `.github/skills/internal-java/SKILL.md` @@ -115,6 +113,8 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/internal-python-project/SKILL.md` - `.github/skills/internal-python-script/SKILL.md` - `.github/skills/internal-python/SKILL.md` +- `.github/skills/internal-review-code/SKILL.md` +- `.github/skills/internal-review-high-level/SKILL.md` - `.github/skills/internal-skill-creator/SKILL.md` - `.github/skills/internal-tdd/SKILL.md` - `.github/skills/internal-terraform/SKILL.md` @@ -167,7 +167,9 @@ These imported `openai-*` office skills remain support-only depth for repositori - `.github/scripts/lib/internal_skills.py` - `.github/scripts/lib/inventory.py` - `.github/scripts/lib/jsonc.py` +- `.github/scripts/lib/repo_paths.py` - `.github/scripts/lib/shared.py` +- `.github/scripts/lib/sync_exclusions.py` - `.github/scripts/lib/syncing.py` - `.github/scripts/lib/token_risks.py` - `.github/scripts/run.sh` @@ -176,11 +178,11 @@ These imported `openai-*` office skills remain support-only depth for repositori ## Agents -- `.github/agents/internal-review-code.agent.md` - `.github/agents/internal-gateway-critical-master.agent.md` - `.github/agents/internal-gateway-idea.agent.md` - `.github/agents/internal-gateway-review.agent.md` - `.github/agents/internal-gateway-simple-task.agent.md` +- `.github/agents/internal-review-code.agent.md` - `.github/agents/local-sync-external-resources.agent.md` - `.github/agents/local-sync-global-copilot-configs-into-repo.agent.md` - `.github/agents/local-sync-install-ai-resources.agent.md` diff --git a/.github/scripts/lib/repo_paths.py b/.github/scripts/lib/repo_paths.py new file mode 100644 index 00000000..da0625c8 --- /dev/null +++ b/.github/scripts/lib/repo_paths.py @@ -0,0 +1,13 @@ +"""Shared repository root discovery.""" + +from __future__ import annotations + +from pathlib import Path + + +def find_repo_root(start: Path) -> Path: + candidate = start.resolve() + for current in (candidate, *candidate.parents): + if (current / ".github").is_dir(): + return current + raise FileNotFoundError(f"Unable to find repository root from {start}") diff --git a/.github/scripts/lib/shared.py b/.github/scripts/lib/shared.py index 923da65e..772ef681 100644 --- a/.github/scripts/lib/shared.py +++ b/.github/scripts/lib/shared.py @@ -190,38 +190,50 @@ def sha256_file(path: Path) -> str: def git_revision(root: Path) -> str | None: - result = subprocess.run( - ["git", "rev-parse", "--short", "HEAD"], - cwd=root, - text=True, - capture_output=True, - check=False, - ) - if result.returncode != 0: + try: + result = subprocess.run( + ["git", "rev-parse", "--short", "HEAD"], + cwd=root, + text=True, + capture_output=True, + check=False, + timeout=10, + ) + if result.returncode != 0: + return None + return result.stdout.strip() or None + except (subprocess.TimeoutExpired, OSError): return None - return result.stdout.strip() or None def is_git_dirty(root: Path) -> bool: - result = subprocess.run( - ["git", "status", "--porcelain"], - cwd=root, - text=True, - capture_output=True, - check=False, - ) - if result.returncode != 0: + try: + result = subprocess.run( + ["git", "status", "--porcelain"], + cwd=root, + text=True, + capture_output=True, + check=False, + timeout=10, + ) + if result.returncode != 0: + return False + return bool(result.stdout.strip()) + except (subprocess.TimeoutExpired, OSError): return False - return bool(result.stdout.strip()) def git_dirty_paths(root: Path) -> list[str]: - result = subprocess.run( - ["git", "status", "--porcelain=v1", "-z", "--untracked-files=all"], - cwd=root, - capture_output=True, - check=False, - ) + try: + result = subprocess.run( + ["git", "status", "--porcelain=v1", "-z", "--untracked-files=all"], + cwd=root, + capture_output=True, + check=False, + timeout=10, + ) + except (subprocess.TimeoutExpired, OSError): + return [] if result.returncode != 0 or not result.stdout: return [] diff --git a/.github/scripts/lib/sync_exclusions.py b/.github/scripts/lib/sync_exclusions.py new file mode 100644 index 00000000..285c73d4 --- /dev/null +++ b/.github/scripts/lib/sync_exclusions.py @@ -0,0 +1,34 @@ +"""Shared sync exclusion rules for runtime artifacts.""" + +from __future__ import annotations + +from pathlib import Path + +IGNORED_SYNC_PARTS: frozenset[str] = frozenset({ + ".venv", + "__pycache__", + ".pytest_cache", +}) + +IGNORED_SYNC_SUFFIXES: frozenset[str] = frozenset({ + ".pyc", + ".pyo", +}) + + +def should_ignore_sync_path(path: Path) -> bool: + parts = path.parts + if any(part in IGNORED_SYNC_PARTS for part in parts): + return True + if path.suffix in IGNORED_SYNC_SUFFIXES: + return True + return False + + +def sync_copytree_ignore(directory: str, names: list[str]) -> set[str]: + ignored: set[str] = set() + for name in names: + candidate = Path(directory) / name + if should_ignore_sync_path(candidate): + ignored.add(name) + return ignored diff --git a/tests/github/scripts/lib/test_repo_paths.py b/tests/github/scripts/lib/test_repo_paths.py new file mode 100644 index 00000000..2011ace6 --- /dev/null +++ b/tests/github/scripts/lib/test_repo_paths.py @@ -0,0 +1,23 @@ +import pytest +from pathlib import Path +import sys + +REPO_ROOT = next( + parent for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +sys.path.insert(0, str(REPO_ROOT / ".github/scripts")) + +from lib.repo_paths import find_repo_root + + +def test_find_repo_root_from_nested_path(tmp_path: Path) -> None: + (tmp_path / ".github").mkdir() + nested = tmp_path / "a" / "b" / "c" + nested.mkdir(parents=True) + assert find_repo_root(nested) == tmp_path + + +def test_find_repo_root_raises_when_not_found(tmp_path: Path) -> None: + with pytest.raises(FileNotFoundError, match="Unable to find repository root"): + find_repo_root(tmp_path) diff --git a/tests/github/scripts/lib/test_subprocess_timeout.py b/tests/github/scripts/lib/test_subprocess_timeout.py new file mode 100644 index 00000000..b10f4f43 --- /dev/null +++ b/tests/github/scripts/lib/test_subprocess_timeout.py @@ -0,0 +1,32 @@ +import subprocess +import sys +from pathlib import Path +from unittest.mock import patch + +import pytest + +REPO_ROOT = next( + parent for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +sys.path.insert(0, str(REPO_ROOT / ".github/scripts")) + +from lib.shared import git_revision, is_git_dirty, git_dirty_paths + + +def test_git_revision_handles_timeout(tmp_path: Path) -> None: + with patch("lib.shared.subprocess.run", side_effect=subprocess.TimeoutExpired("git", 10)): + result = git_revision(tmp_path) + assert result is None + + +def test_is_git_dirty_handles_timeout(tmp_path: Path) -> None: + with patch("lib.shared.subprocess.run", side_effect=subprocess.TimeoutExpired("git", 10)): + result = is_git_dirty(tmp_path) + assert result is False + + +def test_git_dirty_paths_handles_timeout(tmp_path: Path) -> None: + with patch("lib.shared.subprocess.run", side_effect=subprocess.TimeoutExpired("git", 10)): + result = git_dirty_paths(tmp_path) + assert result == [] diff --git a/tests/github/scripts/lib/test_sync_exclusions.py b/tests/github/scripts/lib/test_sync_exclusions.py new file mode 100644 index 00000000..264c9424 --- /dev/null +++ b/tests/github/scripts/lib/test_sync_exclusions.py @@ -0,0 +1,38 @@ +import pytest +from pathlib import Path +import sys + +REPO_ROOT = next( + parent for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +sys.path.insert(0, str(REPO_ROOT / ".github/scripts")) + +from lib.sync_exclusions import ( + IGNORED_SYNC_PARTS, + IGNORED_SYNC_SUFFIXES, + should_ignore_sync_path, +) + + +def test_ignored_parts_contain_venv_and_pycache() -> None: + assert ".venv" in IGNORED_SYNC_PARTS + assert "__pycache__" in IGNORED_SYNC_PARTS + assert ".pytest_cache" in IGNORED_SYNC_PARTS + + +def test_ignored_suffixes_contain_pyc_and_pyo() -> None: + assert ".pyc" in IGNORED_SYNC_SUFFIXES + assert ".pyo" in IGNORED_SYNC_SUFFIXES + + +def test_should_ignore_venv_path() -> None: + assert should_ignore_sync_path(Path(".venv/lib/site.py")) + + +def test_should_ignore_pycache() -> None: + assert should_ignore_sync_path(Path("skills/foo/__pycache__/mod.pyc")) + + +def test_should_not_ignore_normal_path() -> None: + assert not should_ignore_sync_path(Path("skills/foo/SKILL.md")) From ba17eeea8f7fe9be49c27f1c2eb8218bfc6a7f76 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Wed, 8 Jul 2026 21:53:31 +0200 Subject: [PATCH 27/59] feat: update skill documentation for clarity and consistency across internal agent support and changelog automation --- .../internal-agent-support-lane-change-engine/SKILL.md | 10 ++-------- .../skills/internal-agent-support-next-step/SKILL.md | 7 +++---- .github/skills/internal-changelog-automation/SKILL.md | 8 ++++++++ .github/skills/internal-context-handoff/SKILL.md | 2 +- .../internal-java-project/references/examples.md | 2 +- .../internal-nodejs-project/references/examples.md | 6 +++--- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/.github/skills/internal-agent-support-lane-change-engine/SKILL.md b/.github/skills/internal-agent-support-lane-change-engine/SKILL.md index e9eef9e7..4713ccc6 100644 --- a/.github/skills/internal-agent-support-lane-change-engine/SKILL.md +++ b/.github/skills/internal-agent-support-lane-change-engine/SKILL.md @@ -7,10 +7,9 @@ description: Use when a repository-owned internal agent needs a consistent user- ## Referenced skills -- `internal-gateway-review`: source lane when review findings leave a local fix, planning gap, or assumption challenge. - `internal-gateway-simple-task`: source or target lane for concrete local follow-up work. -- `internal-gateway-idea`: target lane when planning or scope definition is needed. -- `internal-gateway-critical-master`: target lane when assumption pressure-testing dominates. +- `internal-gateway-idea`: source lane when planning or scope definition is needed; target lane when planning or scope definition dominates. +- `internal-gateway-critical-master`: source lane when assumption pressure-testing dominates; target lane when assumption pressure-testing dominates. - `internal-gateway-execute-plans`: target lane when approved retained-plan execution is the next step. Use this skill as the shared lane-mismatch engine for repository-owned internal @@ -32,13 +31,8 @@ agents. | Current agent | When the boundary breaks | Recommend | | --- | --- | --- | -| `internal-gateway-review` | Findings now leave only a concrete local fix | `internal-gateway-simple-task` | -| `internal-gateway-review` | Findings reopen planning or scope definition | `internal-gateway-idea` | -| `internal-gateway-review` | Assumption pressure-testing becomes dominant | `internal-gateway-critical-master` | | `internal-gateway-simple-task` | Planning or governance becomes dominant | `internal-gateway-idea` | -| `internal-gateway-simple-task` | Defect-first analysis becomes dominant | `internal-gateway-review` | | `internal-gateway-simple-task` | Assumption pressure-testing becomes dominant | `internal-gateway-critical-master` | | `internal-gateway-critical-master` | The next step is planning | `internal-gateway-idea` | -| `internal-gateway-critical-master` | The next step is evidence-first review | `internal-gateway-review` | | `internal-gateway-idea` | A retained `compact` plan is approved for execution | `internal-gateway-simple-task` | | `internal-gateway-idea` | A retained `extended` plan is approved for execution | `internal-gateway-execute-plans` | diff --git a/.github/skills/internal-agent-support-next-step/SKILL.md b/.github/skills/internal-agent-support-next-step/SKILL.md index 538e0da8..f7847541 100644 --- a/.github/skills/internal-agent-support-next-step/SKILL.md +++ b/.github/skills/internal-agent-support-next-step/SKILL.md @@ -8,7 +8,6 @@ description: Use when a repository-owned agent or prompt needs to package an alr ## Referenced skills - `internal-gateway-idea`: source lane that may need a visible next-step transition. -- `internal-gateway-review`: source lane that may need a visible next-step transition. - `internal-gateway-simple-task`: source lane that may need a visible next-step transition. - `internal-gateway-execute-plans`: source lane that may need a visible next-step transition. - `internal-gateway-critical-master`: source lane that may need a visible next-step transition. @@ -18,9 +17,9 @@ chosen or confirmed. ## When to use -- `internal-gateway-idea`, `internal-gateway-review`, - `internal-gateway-simple-task`, `internal-gateway-execute-plans`, or - `internal-gateway-critical-master` stop with a visible transition. +- `internal-gateway-idea`, `internal-gateway-simple-task`, + `internal-gateway-execute-plans`, or `internal-gateway-critical-master` + stop with a visible transition. ## Package Contract diff --git a/.github/skills/internal-changelog-automation/SKILL.md b/.github/skills/internal-changelog-automation/SKILL.md index 477a8927..1c94c708 100644 --- a/.github/skills/internal-changelog-automation/SKILL.md +++ b/.github/skills/internal-changelog-automation/SKILL.md @@ -52,6 +52,14 @@ Use Keep a Changelog headings where possible: 4. Remove noise such as merge-only or maintenance-only entries that do not matter to readers. 5. Produce release notes and update the changelog consistently. +## Validation + +- Run a dry-run or preview over the target release range before writing the changelog. Confirm the commit range, PR set, and version boundary match expectations. +- After generation, verify that each user-facing change appears exactly once under the correct Keep a Changelog heading. +- Confirm no raw merge commits, dependency bumps, or internal-only entries leak into the user-facing output. +- When the repository has no active changelog generator wired into CI, state the activation gap explicitly and recommend the minimum viable check (for example, a manual review step or a CI lint rule). +- If validation fails, report the specific entries that diverged and do not overwrite the changelog until the input source or filter is corrected. + ## Semver Guidance - Major: breaking changes or removals diff --git a/.github/skills/internal-context-handoff/SKILL.md b/.github/skills/internal-context-handoff/SKILL.md index 34f0aa46..e7e5ea19 100644 --- a/.github/skills/internal-context-handoff/SKILL.md +++ b/.github/skills/internal-context-handoff/SKILL.md @@ -20,7 +20,7 @@ Produce a token-efficient, paste-ready Markdown handoff that another agent can u ## When not to use - The user needs a compact transition between already-selected owners. Use `internal-agent-support-next-step` instead. -- The user needs a Decision Brief for retained planning. Use `internal-agent-support-next-step` references/decision-brief.md. +- The user needs a Decision Brief for retained planning. Use `internal-agent-support-next-step` for the Decision Brief contract. - The task is a gateway phase change or non-terminal exit. Use the owning gateway skill. ## Workflow diff --git a/.github/skills/internal-java-project/references/examples.md b/.github/skills/internal-java-project/references/examples.md index 01bde54c..36e78989 100644 --- a/.github/skills/internal-java-project/references/examples.md +++ b/.github/skills/internal-java-project/references/examples.md @@ -7,7 +7,7 @@ public final class UserService { public String resolveUserId(String userId) { if (userId == null || userId.isBlank()) { - throw new IllegalArgumentException("❌ userId is required"); + throw new IllegalArgumentException("userId is required"); } return userId.trim(); } diff --git a/.github/skills/internal-nodejs-project/references/examples.md b/.github/skills/internal-nodejs-project/references/examples.md index cfa64965..c2c1dcb4 100644 --- a/.github/skills/internal-nodejs-project/references/examples.md +++ b/.github/skills/internal-nodejs-project/references/examples.md @@ -6,7 +6,7 @@ /** Purpose: Build a user profile response. */ function buildUserProfile(input) { if (!input?.id) { - throw new Error("❌ id is required"); + throw new Error("id is required"); } return { id: input.id, name: input.name ?? "unknown" }; } @@ -15,8 +15,8 @@ function buildUserProfile(input) { ## Minimal Test Example ```javascript -const test = require("node:test"); -const assert = require("node:assert/strict"); +import { test } from "node:test"; +import assert from "node:assert/strict"; test("given missing id when building profile then throws", () => { assert.throws(() => buildUserProfile({}), /id is required/); From 96641288622d3865efa1fb3858252dd3db31da7f Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Wed, 8 Jul 2026 22:40:34 +0200 Subject: [PATCH 28/59] Add new anti-pattern references and update inventory - Added `mattpocock-code-review` skill for two-axis code reviews. - Updated inventory to include new skills and references. - Introduced anti-pattern catalogs for Bash, Java, Node.js, Python, and Terraform. - Enhanced `internal-review-code` to load relevant anti-pattern references. - Updated managed resource scope to reflect new skill imports. - Corrected file paths in tests to point to the new anti-pattern references. --- .github/INVENTORY.md | 1 + .../local-sync-external-resources.agent.md | 9 +- .github/skills/internal-bash/SKILL.md | 4 + .../references/review-anti-patterns.md} | 0 .github/skills/internal-java/SKILL.md | 4 + .../references/review-anti-patterns.md} | 0 .github/skills/internal-nodejs/SKILL.md | 4 + .../references/review-anti-patterns.md} | 0 .github/skills/internal-python/SKILL.md | 4 + .../references/review-anti-patterns.md} | 0 .github/skills/internal-review-code/SKILL.md | 98 +++++++------------ .../internal-review-code/agents/openai.yaml | 4 +- .github/skills/internal-terraform/SKILL.md | 4 + .../references/review-anti-patterns.md} | 0 .../references/managed-resource-scope.md | 2 +- .../skills/mattpocock-code-review/SKILL.md | 89 +++++++++++++++++ tests/test_repository_test_layout_contract.py | 2 +- 17 files changed, 156 insertions(+), 69 deletions(-) rename .github/skills/{internal-review-code/references/anti-patterns-bash.md => internal-bash/references/review-anti-patterns.md} (100%) rename .github/skills/{internal-review-code/references/anti-patterns-java.md => internal-java/references/review-anti-patterns.md} (100%) rename .github/skills/{internal-review-code/references/anti-patterns-nodejs.md => internal-nodejs/references/review-anti-patterns.md} (100%) rename .github/skills/{internal-review-code/references/anti-patterns-python.md => internal-python/references/review-anti-patterns.md} (100%) rename .github/skills/{internal-review-code/references/anti-patterns-terraform.md => internal-terraform/references/review-anti-patterns.md} (100%) create mode 100644 .github/skills/mattpocock-code-review/SKILL.md diff --git a/.github/INVENTORY.md b/.github/INVENTORY.md index 9e458025..6b12c5c3 100644 --- a/.github/INVENTORY.md +++ b/.github/INVENTORY.md @@ -124,6 +124,7 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/local-agent-sync-install-ai-resources/SKILL.md` - `.github/skills/local-copilot-log-analyzer/SKILL.md` - `.github/skills/mattpocock-caveman/SKILL.md` +- `.github/skills/mattpocock-code-review/SKILL.md` - `.github/skills/openai-docx/SKILL.md` - `.github/skills/openai-gh-address-comments/SKILL.md` - `.github/skills/openai-gh-fix-ci/SKILL.md` diff --git a/.github/agents/local-sync-external-resources.agent.md b/.github/agents/local-sync-external-resources.agent.md index 2aa0882e..0821d708 100644 --- a/.github/agents/local-sync-external-resources.agent.md +++ b/.github/agents/local-sync-external-resources.agent.md @@ -74,10 +74,11 @@ retire, and anti-drift procedure in the paired core skill. `.github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md` for the exact upstream family map, retained support-only office posture, and approved imported-override context. -- Active `mattpocock/skills` imports in scope are `grill-me` -> `grill-me` - plus retained `caveman` -> `mattpocock-caveman` from the previous managed - snapshot while `caveman` is absent from current upstream; keep retired Matt - Pocock imports out of the live managed scope. +- Active `mattpocock/skills` imports in scope are `code-review` -> + `mattpocock-code-review`, `grill-me` -> `grill-me`, plus retained `caveman` + -> `mattpocock-caveman` from the previous managed snapshot while `caveman` + is absent from current upstream; keep retired Matt Pocock imports out of the + live managed scope. - Do not add new prefixes, external families, compatibility aliases, or hidden imported forks unless the user explicitly expands scope. - When catalog meaning changes, re-check root `AGENTS.md`, diff --git a/.github/skills/internal-bash/SKILL.md b/.github/skills/internal-bash/SKILL.md index 51c9b546..e4c9e0ed 100644 --- a/.github/skills/internal-bash/SKILL.md +++ b/.github/skills/internal-bash/SKILL.md @@ -13,6 +13,10 @@ safety to standalone script behavior. - `internal-bash-script`: standalone Bash scripts, shell utilities, wrappers, launchers, and operator-facing behavior. +## Referenced files + +- `references/review-anti-patterns.md`: Bash review anti-pattern catalog with ID-tagged patterns, severity, rationale, and examples. Load when `internal-review-code` or a review-oriented caller needs Bash-specific defect depth. + ## When to use - `.sh` files and Bash snippets where the main need is a shared safety baseline. diff --git a/.github/skills/internal-review-code/references/anti-patterns-bash.md b/.github/skills/internal-bash/references/review-anti-patterns.md similarity index 100% rename from .github/skills/internal-review-code/references/anti-patterns-bash.md rename to .github/skills/internal-bash/references/review-anti-patterns.md diff --git a/.github/skills/internal-java/SKILL.md b/.github/skills/internal-java/SKILL.md index 34876041..06403a6d 100644 --- a/.github/skills/internal-java/SKILL.md +++ b/.github/skills/internal-java/SKILL.md @@ -13,6 +13,10 @@ every Java edit; load them only when the task proves which owner is needed. - `internal-java-project`: Java package, module, service, library, and deterministic test design when application or library structure becomes the main concern. - `internal-java-spring-boot-development`: Spring Boot controllers, configuration, repositories, scheduled jobs, and framework tests when framework behavior becomes the main concern. +## Referenced files + +- `references/review-anti-patterns.md`: Java review anti-pattern catalog with ID-tagged patterns, severity, rationale, and examples. Load when `internal-review-code` or a review-oriented caller needs Java-specific defect depth. + ## When to use - `.java`, `pom.xml`, `build.gradle`, or `build.gradle.kts` changes. diff --git a/.github/skills/internal-review-code/references/anti-patterns-java.md b/.github/skills/internal-java/references/review-anti-patterns.md similarity index 100% rename from .github/skills/internal-review-code/references/anti-patterns-java.md rename to .github/skills/internal-java/references/review-anti-patterns.md diff --git a/.github/skills/internal-nodejs/SKILL.md b/.github/skills/internal-nodejs/SKILL.md index afa0272b..944ba74c 100644 --- a/.github/skills/internal-nodejs/SKILL.md +++ b/.github/skills/internal-nodejs/SKILL.md @@ -13,6 +13,10 @@ package architecture, or deterministic test design becomes the real issue. - `internal-nodejs-project`: Node.js or TypeScript services, APIs, middleware, modules, and deterministic tests when project-level architecture becomes the main concern. +## Referenced files + +- `references/review-anti-patterns.md`: Node.js/TypeScript review anti-pattern catalog with ID-tagged patterns, severity, rationale, and examples. Load when `internal-review-code` or a review-oriented caller needs Node.js-specific defect depth. + ## When to use - `.js`, `.cjs`, `.mjs`, `.ts`, `.tsx`, `package.json`, `tsconfig.json`, or Node.js package-manager lockfile changes. diff --git a/.github/skills/internal-review-code/references/anti-patterns-nodejs.md b/.github/skills/internal-nodejs/references/review-anti-patterns.md similarity index 100% rename from .github/skills/internal-review-code/references/anti-patterns-nodejs.md rename to .github/skills/internal-nodejs/references/review-anti-patterns.md diff --git a/.github/skills/internal-python/SKILL.md b/.github/skills/internal-python/SKILL.md index eb08078d..6d9ebf32 100644 --- a/.github/skills/internal-python/SKILL.md +++ b/.github/skills/internal-python/SKILL.md @@ -13,6 +13,10 @@ every Python edit; load them only when the task proves script or project depth. - `internal-python-script`: standalone Python scripts, CLIs, and operator-facing toolkits when dependency bootstrap, launcher behavior, or direct execution becomes the main concern. - `internal-python-project`: Python packages, application code, service boundaries, and framework-owned flows when importable behavior or service structure becomes the main concern. +## Referenced files + +- `references/review-anti-patterns.md`: Python review anti-pattern catalog with ID-tagged patterns, severity, rationale, and examples. Load when `internal-review-code` or a review-oriented caller needs Python-specific defect depth. + ## When to use - `.py` changes where the first need is the shared Python baseline. diff --git a/.github/skills/internal-review-code/references/anti-patterns-python.md b/.github/skills/internal-python/references/review-anti-patterns.md similarity index 100% rename from .github/skills/internal-review-code/references/anti-patterns-python.md rename to .github/skills/internal-python/references/review-anti-patterns.md diff --git a/.github/skills/internal-review-code/SKILL.md b/.github/skills/internal-review-code/SKILL.md index 42ae594e..77e34e6b 100644 --- a/.github/skills/internal-review-code/SKILL.md +++ b/.github/skills/internal-review-code/SKILL.md @@ -7,20 +7,30 @@ description: Use when review evidence needs line-level or language-specific defe ## Referenced skills -This index lists every other skill that this file asks the agent to load, route to, compare against, or delegate to. - -- `antigravity-golang-pro`: Go concurrency, service design, or performance specialist when Go dominates the review. -- `awesome-copilot-codeql`: CodeQL workflow and SARIF specialist when CodeQL setup dominates the review. -- `awesome-copilot-secret-scanning`: GitHub secret scanning specialist when secret scanning workflow dominates the review. +- `mattpocock-code-review`: imported two-axis review core (Standards + Spec) for diff-based parallel sub-agent review. - `internal-review-high-level`: systems-level review beyond line-level defects. -- `internal-terraform`: Terraform specialist when HCL modeling or drift-safe Terraform dominates the review. +- `internal-python`: Python anti-pattern depth (load the review anti-patterns reference from that skill). +- `internal-bash`: Bash anti-pattern depth (load the review anti-patterns reference from that skill). +- `internal-terraform`: Terraform anti-pattern depth (load the review anti-patterns reference from that skill). +- `internal-java`: Java anti-pattern depth (load the review anti-patterns reference from that skill). +- `internal-nodejs`: Node.js anti-pattern depth (load the review anti-patterns reference from that skill). - `superpowers-verification-before-completion`: evidence gate before claiming no findings or merge readiness. ## When to use -- Perform an exhaustive, nit-level code review on Python, Bash, Terraform, Java, or Node.js/TypeScript files. +- Perform a line-level code review on Python, Bash, Terraform, Java, or Node.js/TypeScript files. - Provide structured findings with per-language anti-pattern detection. -- Complement specialist reviewer agents with deep language-specific checks. +- The diff or code change is the primary review surface. + +## When not to use + +- The primary target is an AI resource, workflow, policy, plan, or documentation package; use `internal-gateway-review` instead. +- The review needs systems-level architectural analysis; use `internal-review-high-level` instead. +- The user asks for a two-axis Standards + Spec parallel review; consult `mattpocock-code-review` as the imported core. + +## Role + +`internal-review-code` is the only repo-owned line-level review contract. It owns trigger, boundary, severity, output shape, and validation discipline. Language-specific anti-pattern depth lives in the nearest language owner, not in this wrapper. ## Standalone quick start @@ -57,26 +67,12 @@ Establish these review inputs before grading the diff: ## Escalation rules -- Any single anti-pattern repeated three or more times in the same diff escalates one severity level (e.g., `Nit` → `Minor`, `Minor` → `Major`). -- Any deviation from the matching language or domain skill baseline is at minimum a `Nit`. +- Any single anti-pattern repeated three or more times in the same diff escalates one severity level. +- Any deviation from the matching language skill baseline is at minimum a `Nit`. - Any violation of `security-baseline.md` is at minimum a `Major`. -## Anti-pattern catalogs - -Per-language catalogs with ID-tagged patterns, severity, rationale, and good/bad examples are in `references/`: - -- `references/anti-patterns-python.md` — PY-C01..PY-N06 -- `references/anti-patterns-bash.md` — SH-C01..SH-N05 -- `references/anti-patterns-terraform.md` — TF-C01..TF-N05 -- `references/anti-patterns-java.md` — JV-C01..JV-N04 -- `references/anti-patterns-nodejs.md` — ND-C01..ND-N04 - -Load the relevant catalog(s) based on file extensions detected in the diff. - ## Cross-language checks -These apply regardless of language: - | Severity | Check | | --- | --- | | `Critical` | Hardcoded secrets, tokens, passwords, or API keys | @@ -88,7 +84,7 @@ These apply regardless of language: ## Review lenses -Always cover these dimensions, even when the language-specific catalog is the primary tool: +Always cover these dimensions: - Functionality: correctness, edge cases, failure handling, and requirement fit - Security: input validation, secret handling, privilege boundaries, unsafe interpolation, and dependency risk @@ -105,57 +101,37 @@ When the review also asks whether the change can be simplified safely, use this - Efficiency: flag repeated work, duplicate reads, unnecessary recomputation, and overly broad scans that add cost without benefit. - Clarity: flag deep nesting, weak naming, dead code, redundant comments, and indirection that no longer earns its keep. -Only elevate simplification suggestions when they materially improve maintainability, correctness, or cost. Do not churn code for aesthetic reasons alone. +Only elevate simplification suggestions when they materially improve maintainability, correctness, or cost. ## Review workflow -1. **Identify languages and changed surfaces** in the diff (auto-detect from file extensions and changed paths). -2. **Read enough nearby context** to understand intent, requirements, and test strategy before judging style or structure. -3. **Load applicable anti-pattern catalogs** from `references/`. -4. **Scan each changed file** against the relevant catalog. +1. **Identify languages and changed surfaces** in the diff. +2. **Read enough nearby context** to understand intent, requirements, and test strategy. +3. **Load applicable anti-pattern references** from the nearest language owner. +4. **Scan each changed file** against the relevant anti-pattern reference. 5. **Cross-check the review lenses** for functionality, security, performance, tests, and maintainability. -6. **Self-question each finding**: Is this really wrong, or am I misunderstanding the context? Could the author have a valid reason? +6. **Self-question each finding**: Is this really wrong, or am I misunderstanding the context? 7. **Apply escalation rules** for repeated violations. -8. **Group findings** by severity: `Critical` → `Major` → `Minor` → `Nit` → `Notes`. +8. **Group findings** by severity: `Critical` -> `Major` -> `Minor` -> `Nit` -> `Notes`. 9. **Include file path and line reference** for every finding. -10. **Suggest a concrete fix** or reference the "good" example for each finding. +10. **Suggest a concrete fix** for each finding. 11. **Summarize** total finding count per severity at the end. -## Common mistakes - -| Mistake | Why it matters | Instead | -| --- | --- | --- | -| Flagging style issues as Major | Dilutes urgency of real problems | Use severity mappings strictly | -| Reviewing only the diff without reading surrounding context | Missing that the "bad" pattern is intentional for backward compat | Read 20-30 lines before/after each change | -| Skipping the requirements or test strategy first | You can flag a deliberate tradeoff as a defect | Establish the context checklist before scoring findings | -| Applying Python rules to Bash or vice versa | Different idioms, different expectations | Always check the file extension first | -| Reporting "missing tests" without checking if tests exist elsewhere | False findings erode trust | Search for test files before flagging | -| Skipping cross-language checks | Secrets and validation gaps cross all languages | Always run the cross-language table | -| Generic "this could be improved" without concrete fix | Not actionable | Every finding must include a fix suggestion | - -## Cross-references - -- **internal-review-high-level**: for systems-level impact analysis, architectural evaluation, workflow impact, and blind-spot detection beyond line-level review. -- Use both together: this skill for nit-level anti-patterns first, then `internal-review-high-level` for the bigger picture. -- **superpowers-verification-before-completion**: for evidence before claiming no - findings, review completion, or merge readiness. - ## Delegation -Use this skill as the default review owner, then add a narrower specialist only when the evidence demands it. +Stay with `internal-review-code` when the main need is defect-first review across mixed Python, Bash, Terraform, Java, or Node.js changes. -- Stay with `internal-review-code` when the main need is defect-first review across mixed Python, Bash, Terraform, Java, or Node.js changes. +- Add `mattpocock-code-review` when the user explicitly asks for a two-axis Standards + Spec parallel review. - Add `internal-terraform` when the review is primarily about Terraform resource modeling, module interfaces, or drift-safe HCL changes. - Add `antigravity-golang-pro` when the review is primarily about Go concurrency, service design, or Go performance behavior. -- Add `awesome-copilot-codeql` when the review is primarily about CodeQL workflow setup, SARIF handling, or query-suite coverage rather than the diff itself. -- Add `awesome-copilot-secret-scanning` when the review is primarily about GitHub-native secret scanning, push protection, alert handling, or blocked-push remediation. -- Add language or domain specialists only when they materially improve the finding quality; do not fan out by default. +- Add `awesome-copilot-codeql` when the review is primarily about CodeQL workflow setup or SARIF handling. +- Add `awesome-copilot-secret-scanning` when the review is primarily about GitHub-native secret scanning or push protection. +- Add language or domain specialists only when they materially improve the finding quality. ## Validation - Verify every finding references a real file path and line from the diff. -- Verify severity assignments match the anti-pattern catalog rules. -- Verify escalation rules are applied for repeated violations (3+ of the same kind). +- Verify severity assignments match the anti-pattern reference rules. +- Verify escalation rules are applied for repeated violations. - Verify cross-language checks are applied regardless of primary language. -- Use `superpowers-verification-before-completion` before claiming there are no - findings, the review is complete, or the change is merge-ready. +- Use `superpowers-verification-before-completion` before claiming there are no findings, the review is complete, or the change is merge-ready. diff --git a/.github/skills/internal-review-code/agents/openai.yaml b/.github/skills/internal-review-code/agents/openai.yaml index e45e8fb1..b7a41c74 100644 --- a/.github/skills/internal-review-code/agents/openai.yaml +++ b/.github/skills/internal-review-code/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Internal Review Code" - short_description: "Help with Internal Review Code tasks" - default_prompt: "Use $internal-review-code for this task and follow the repository-owned workflow in the skill." + short_description: "Line-level code review wrapper" + default_prompt: "Use $internal-review-code for line-level review of code changes; load language anti-pattern references from the nearest language owner as needed." diff --git a/.github/skills/internal-terraform/SKILL.md b/.github/skills/internal-terraform/SKILL.md index 9caebf74..37699f5c 100644 --- a/.github/skills/internal-terraform/SKILL.md +++ b/.github/skills/internal-terraform/SKILL.md @@ -14,6 +14,10 @@ Terraform-native test behavior becomes part of the task. - `terraform-terraform-search-import`: unmanaged-resource discovery and Terraform import workflows when existing resources must enter Terraform state. - `terraform-terraform-test`: `.tftest.hcl` coverage, mocks, and Terraform test execution when Terraform-native testing is part of the job. +## Referenced files + +- `references/review-anti-patterns.md`: Terraform review anti-pattern catalog with ID-tagged patterns, severity, rationale, and examples. Load when `internal-review-code` or a review-oriented caller needs Terraform-specific defect depth. + ## When to use - Add or modify resources, variables, outputs, data sources in existing configurations. diff --git a/.github/skills/internal-review-code/references/anti-patterns-terraform.md b/.github/skills/internal-terraform/references/review-anti-patterns.md similarity index 100% rename from .github/skills/internal-review-code/references/anti-patterns-terraform.md rename to .github/skills/internal-terraform/references/review-anti-patterns.md diff --git a/.github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md b/.github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md index a3ea53b2..7b010e04 100644 --- a/.github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md +++ b/.github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md @@ -102,7 +102,7 @@ Sources: Managed skills: -- `grill-me` -> `grill-me`. +- `code-review` -> `mattpocock-code-review`; `grill-me` -> `grill-me`. - `caveman` -> `mattpocock-caveman` remains retained from the previous managed snapshot because `caveman` is absent from the current `mattpocock/skills` HEAD. diff --git a/.github/skills/mattpocock-code-review/SKILL.md b/.github/skills/mattpocock-code-review/SKILL.md new file mode 100644 index 00000000..f8f16107 --- /dev/null +++ b/.github/skills/mattpocock-code-review/SKILL.md @@ -0,0 +1,89 @@ +--- +name: mattpocock-code-review +description: Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes — Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/PRD asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to "review since X". +--- + +Two-axis review of the diff between `HEAD` and a fixed point the user supplies: + +- **Standards** — does the code conform to this repo's documented coding standards? +- **Spec** — does the code faithfully implement the originating issue / PRD / spec? + +Both axes run as **parallel sub-agents** so they don't pollute each other's context, then this skill aggregates their findings. + +The issue tracker should have been provided to you — run `/setup-matt-pocock-skills` if `docs/agents/issue-tracker.md` is missing. + +## Process + +### 1. Pin the fixed point + +Whatever the user said is the fixed point — a commit SHA, branch name, tag, `main`, `HEAD~5`, etc. If they didn't specify one, ask for it. + +Capture the diff command once: `git diff ...HEAD` (three-dot, so the comparison is against the merge-base). Also note the list of commits via `git log ..HEAD --oneline`. + +Before going further, confirm the fixed point resolves (`git rev-parse `) and the diff is non-empty. A bad ref or empty diff should fail here — not inside two parallel sub-agents. + +### 2. Identify the spec source + +Look for the originating spec, in this order: + +1. Issue references in the commit messages (`#123`, `Closes #45`, GitLab `!67`, etc.) — fetch via the workflow in `docs/agents/issue-tracker.md`. +2. A path the user passed as an argument. +3. A PRD/spec file under `docs/`, `specs/`, or `.scratch/` matching the branch name or feature. +4. If nothing is found, ask the user where the spec is. If they say there isn't one, the **Spec** sub-agent will skip and report "no spec available". + +### 3. Identify the standards sources + +Anything in the repo that documents how code should be written, such as `CODING_STANDARDS.md` or `CONTRIBUTING.md`. + +On top of whatever the repo documents, the Standards axis always carries the **smell baseline** below — a fixed set of Fowler code smells (_Refactoring_, ch.3) that applies even when a repo documents nothing. Two rules bind it: + +- **The repo overrides.** A documented repo standard always wins; where it endorses something the baseline would flag, suppress the smell. +- **Always a judgement call.** Each smell is a labelled heuristic ("possible Feature Envy"), never a hard violation — and, like any standard here, skip anything tooling already enforces. + +Each smell reads *what it is* → *how to fix*; match it against the diff: + +- **Mysterious Name** — a function, variable, or type whose name doesn't reveal what it does or holds. → rename it; if no honest name comes, the design's murky. +- **Duplicated Code** — the same logic shape appears in more than one hunk or file in the change. → extract the shared shape, call it from both. +- **Feature Envy** — a method that reaches into another object's data more than its own. → move the method onto the data it envies. +- **Data Clumps** — the same few fields or params keep travelling together (a type wanting to be born). → bundle them into one type, pass that. +- **Primitive Obsession** — a primitive or string standing in for a domain concept that deserves its own type. → give the concept its own small type. +- **Repeated Switches** — the same `switch`/`if`-cascade on the same type recurs across the change. → replace with polymorphism, or one map both sites share. +- **Shotgun Surgery** — one logical change forces scattered edits across many files in the diff. → gather what changes together into one module. +- **Divergent Change** — one file or module is edited for several unrelated reasons. → split so each module changes for one reason. +- **Speculative Generality** — abstraction, parameters, or hooks added for needs the spec doesn't have. → delete it; inline back until a real need shows. +- **Message Chains** — long `a.b().c().d()` navigation the caller shouldn't depend on. → hide the walk behind one method on the first object. +- **Middle Man** — a class or function that mostly just delegates onward. → cut it, call the real target direct. +- **Refused Bequest** — a subclass or implementer that ignores or overrides most of what it inherits. → drop the inheritance, use composition. + +### 4. Spawn both sub-agents in parallel + +Send a single message with two `Agent` tool calls. Use the `general-purpose` subagent for both. + +**Standards sub-agent prompt** — include: + +- The full diff command and commit list. +- The list of standards-source files you found in step 3, **plus the smell baseline from step 3** pasted in full — the sub-agent has no other access to it. +- The brief: "Report — per file/hunk where relevant — (a) every place the diff violates a documented standard: cite the standard (file + the rule); and (b) any baseline smell you spot: name it and quote the hunk. Distinguish hard violations from judgement calls — documented-standard breaches can be hard, but baseline smells are always judgement calls, and a documented repo standard overrides the baseline. Skip anything tooling enforces. Under 400 words." + +**Spec sub-agent prompt** — include: + +- The diff command and commit list. +- The path or fetched contents of the spec. +- The brief: "Report: (a) requirements the spec asked for that are missing or partial; (b) behaviour in the diff that wasn't asked for (scope creep); (c) requirements that look implemented but where the implementation looks wrong. Quote the spec line for each finding. Under 400 words." + +If the spec is missing, skip the Spec sub-agent and note this in the final report. + +### 5. Aggregate + +Present the two reports under `## Standards` and `## Spec` headings, verbatim or lightly cleaned. Do **not** merge or rerank findings — the two axes are deliberately separate (see _Why two axes_). + +End with a one-line summary: total findings per axis, and the worst issue _within each axis_ (if any). Don't pick a single winner across axes — that's the reranking the separation exists to prevent. + +## Why two axes + +A change can pass one axis and fail the other: + +- Code that follows every standard but implements the wrong thing → **Standards pass, Spec fail.** +- Code that does exactly what the issue asked but breaks the project's conventions → **Spec pass, Standards fail.** + +Reporting them separately stops one axis from masking the other. diff --git a/tests/test_repository_test_layout_contract.py b/tests/test_repository_test_layout_contract.py index d4afdebb..f66a825a 100644 --- a/tests/test_repository_test_layout_contract.py +++ b/tests/test_repository_test_layout_contract.py @@ -12,7 +12,7 @@ INTERNAL_TDD = REPO_ROOT / ".github/skills/internal-tdd/SKILL.md" INTERNAL_CONTRACT = REPO_ROOT / "INTERNAL_CONTRACT.md" ANTI_PATTERNS = ( - REPO_ROOT / ".github/skills/internal-review-code/references/anti-patterns-python.md" + REPO_ROOT / ".github/skills/internal-python/references/review-anti-patterns.md" ) SKILL_PATH_PATTERN = re.compile(r"\.github/skills/([^/]+)/") From 85da5ca819cea6b1a601548c470261c7b18c8dfd Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 10:28:24 +0200 Subject: [PATCH 29/59] feat: retire 'mattpocock-caveman' skill and update related documentation for clarity --- .github/INVENTORY.md | 1 - .../local-sync-external-resources.agent.md | 12 +++-- .../SKILL.md | 4 -- .../references/external-watchlist.yaml | 7 +++ .../references/managed-resource-scope.md | 28 +++++++++-- .../SKILL.md | 4 +- .github/skills/mattpocock-caveman/SKILL.md | 49 ------------------- 7 files changed, 40 insertions(+), 65 deletions(-) delete mode 100644 .github/skills/mattpocock-caveman/SKILL.md diff --git a/.github/INVENTORY.md b/.github/INVENTORY.md index 6b12c5c3..83384bcb 100644 --- a/.github/INVENTORY.md +++ b/.github/INVENTORY.md @@ -123,7 +123,6 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/local-agent-sync-global-copilot-configs-into-repo/SKILL.md` - `.github/skills/local-agent-sync-install-ai-resources/SKILL.md` - `.github/skills/local-copilot-log-analyzer/SKILL.md` -- `.github/skills/mattpocock-caveman/SKILL.md` - `.github/skills/mattpocock-code-review/SKILL.md` - `.github/skills/openai-docx/SKILL.md` - `.github/skills/openai-gh-address-comments/SKILL.md` diff --git a/.github/agents/local-sync-external-resources.agent.md b/.github/agents/local-sync-external-resources.agent.md index 0821d708..f77d43ea 100644 --- a/.github/agents/local-sync-external-resources.agent.md +++ b/.github/agents/local-sync-external-resources.agent.md @@ -75,10 +75,14 @@ retire, and anti-drift procedure in the paired core skill. for the exact upstream family map, retained support-only office posture, and approved imported-override context. - Active `mattpocock/skills` imports in scope are `code-review` -> - `mattpocock-code-review`, `grill-me` -> `grill-me`, plus retained `caveman` - -> `mattpocock-caveman` from the previous managed snapshot while `caveman` - is absent from current upstream; keep retired Matt Pocock imports out of the - live managed scope. + `mattpocock-code-review`, `grill-me` -> `grill-me`, + `handoff` -> `mattpocock-handoff` (productivity), and `research` -> + `mattpocock-research` (engineering); keep retired Matt Pocock imports + (including the previously retained `caveman` -> `mattpocock-caveman`) + out of the live managed scope and tracked only in the alert-only watchlist. +- Active `vercel-labs/skills` imports in scope are `find-skills` -> + `vercel-find-skills`; the `vercel` prefix is narrow to this family and must + not be reused for other imported families. - Do not add new prefixes, external families, compatibility aliases, or hidden imported forks unless the user explicitly expands scope. - When catalog meaning changes, re-check root `AGENTS.md`, diff --git a/.github/skills/local-agent-sync-external-resources/SKILL.md b/.github/skills/local-agent-sync-external-resources/SKILL.md index ddf84ecb..16850988 100644 --- a/.github/skills/local-agent-sync-external-resources/SKILL.md +++ b/.github/skills/local-agent-sync-external-resources/SKILL.md @@ -10,7 +10,6 @@ description: Use when maintaining the sync-managed `.github/` catalog behind `lo - `internal-skill-creator`: route repository-owned skill creation, replacement, or material rewriting. - `openai-skill-creator`: use only for remaining bundle anatomy after local skill boundaries are settled. - `internal-agent-creator`: route external-resources agent changes or agent/skill boundary rewrites. -- `mattpocock-caveman`: optional compression support after sync gates and evidence are clear. Use this skill as the default operating engine for `.github/agents/local-sync-external-resources.agent.md`. @@ -28,8 +27,6 @@ Use `openai-skill-creator` only for the remaining bundle anatomy, helper scripts Use `internal-agent-creator` when the sync changes this external-resources agent, rewrites agent routing boundaries, or changes how the agent/skill split is structured. -Use `mattpocock-caveman` only as optional compression support for long sync summaries after blockers, warnings, approvals, validation evidence, and destructive-operation gates are already clear. - When deterministic change detection matters, read `references/fingerprinting-contract.md`, use the canonical repository implementation in `.github/scripts/lib/fingerprinting.py`, and use `scripts/sync_resource_fingerprint.py` from this skill only as a thin workflow wrapper. When an imported upstream asset has an exceptionally approved repo-local override, read `references/imported-asset-overrides.yaml` and use `scripts/apply_imported_asset_overrides.py` to replay the registered patch bundle after refreshing the upstream content. @@ -202,7 +199,6 @@ After catalog changes: - Re-check `.github/INVENTORY.md` for exact path accuracy. - Re-check nearby agents and skills for stale references, decorative declarations, or broken ownership assumptions. - Re-check `LESSONS_LEARNED.md` and ask whether any pending lesson was just codified or disproven so the ledger does not retain a duplicate. -- Use compression support only after safety-critical sync details remain explicit. - Use the external watchlist only to notice upstream changes that might deserve a future human review; do not treat watchlist entries as managed scope. diff --git a/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml b/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml index bd2b8e8c..8bf7068e 100644 --- a/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml +++ b/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml @@ -50,6 +50,13 @@ items: local_owner: local-agent-sync-external-resources reason: Setup conventions are not part of the managed source catalog. action: alert-only + - source_family: mattpocock/skills + upstream_id: caveman + local_owner: retired + reason: The previously retained `caveman` -> `mattpocock-caveman` import + was retired from the managed catalog by explicit user scope reduction. + Do not reimport unless the user re-approves the retained import. + action: alert-only - source_family: addyosmani/agent-skills upstream_id: idea-refine local_owner: internal-gateway-idea diff --git a/.github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md b/.github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md index 7b010e04..792e85d6 100644 --- a/.github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md +++ b/.github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md @@ -102,10 +102,15 @@ Sources: Managed skills: -- `code-review` -> `mattpocock-code-review`; `grill-me` -> `grill-me`. -- `caveman` -> `mattpocock-caveman` remains retained from the previous - managed snapshot because `caveman` is absent from the current - `mattpocock/skills` HEAD. +- `code-review` -> `mattpocock-code-review`; `grill-me` -> `grill-me`; + `handoff` -> `mattpocock-handoff` (productivity, compact conversation + handoff for a fresh agent); `research` -> + `mattpocock-research` (engineering, background-agent primary-source + investigation). +- `caveman` -> `mattpocock-caveman` was retired from the managed catalog + by explicit user scope reduction; see `references/external-watchlist.yaml` + for the alert-only retired entry. Do not reimport `caveman` unless the + user re-approves the retained import. Approved in-place overrides: @@ -117,6 +122,21 @@ Approved in-place overrides: Retired upstream items that were extracted into internal owners are tracked in the alert-only watchlist owned by `local-agent-sync-external-resources`. +### `vercel-labs/skills` + +Source: + +- Skills: + `https://github.com/vercel-labs/skills/tree/4ce6d48ac44c8b637db87b2102fea3baca719df1/skills` + (commit date: 2026-07-06) + +Managed skills: + +- `find-skills` -> `vercel-find-skills` (helps users discover and install + agent skills from the open agent skills ecosystem). The local prefix is + `vercel`; keep this prefix narrow to the `vercel-labs/skills` family and + do not reuse it for other imported families. + ### `openai/skills` Sources: diff --git a/.github/skills/local-agent-sync-global-copilot-configs-into-repo/SKILL.md b/.github/skills/local-agent-sync-global-copilot-configs-into-repo/SKILL.md index 57e66477..5b93d190 100644 --- a/.github/skills/local-agent-sync-global-copilot-configs-into-repo/SKILL.md +++ b/.github/skills/local-agent-sync-global-copilot-configs-into-repo/SKILL.md @@ -7,14 +7,12 @@ description: Use when aligning a consumer repository to this repository's manage ## Referenced skills -- `mattpocock-caveman`: optional compression support after target-drift or sync gates are clear. +- None. Use this skill as the mandatory operating engine for `.github/agents/local-sync-global-copilot-configs-into-repo.agent.md`. This skill owns the reusable sync procedure. Keep the paired agent short; do not duplicate the analyze, plan, apply, reporting, or automation rules there. -Use `mattpocock-caveman` only as optional compression support for long target-drift or sync reports after blockers, warnings, approval posture, validation evidence, and destructive-operation gates remain explicit. - The paired agent should not restate default mode handling, preserved `local-*` behavior, `internal-sync-*` exclusions, plan-file lifecycle, or automation entrypoints from this skill. ## When to use diff --git a/.github/skills/mattpocock-caveman/SKILL.md b/.github/skills/mattpocock-caveman/SKILL.md deleted file mode 100644 index a2d8bde4..00000000 --- a/.github/skills/mattpocock-caveman/SKILL.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -name: mattpocock-caveman -description: > - Ultra-compressed communication mode. Cuts token usage ~75% by dropping - filler, articles, and pleasantries while keeping full technical accuracy. - Use when user says "caveman mode", "talk like caveman", "use caveman", - "less tokens", "be brief", or invokes /caveman. ---- - -Respond terse like smart caveman. All technical substance stay. Only fluff die. - -## Persistence - -ACTIVE EVERY RESPONSE once triggered. No revert after many turns. No filler drift. Still active if unsure. Off only when user says "stop caveman" or "normal mode". - -## Rules - -Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Abbreviate common terms (DB/auth/config/req/res/fn/impl). Strip conjunctions. Use arrows for causality (X -> Y). One word when one word enough. - -Technical terms stay exact. Code blocks unchanged. Errors quoted exact. - -Pattern: `[thing] [action] [reason]. [next step].` - -Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..." -Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:" - -### Examples - -**"Why React component re-render?"** - -> Inline obj prop -> new ref -> re-render. `useMemo`. - -**"Explain database connection pooling."** - -> Pool = reuse DB conn. Skip handshake -> fast under load. - -## Auto-Clarity Exception - -Drop caveman temporarily for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done. - -Example -- destructive op: - -> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone. -> -> ```sql -> DROP TABLE users; -> ``` -> -> Caveman resume. Verify backup exist first. From f392762a2093cc8ffe67588c364a00a1e033030f Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 12:45:38 +0200 Subject: [PATCH 30/59] refactor: consolidate external resource manifest --- .../references/managed-resources.yaml | 220 ++++++++++++++++++ .../scripts/sync_external_resources_core.py | 191 +++++++++++++++ .../scripts/test_manifest.py | 74 ++++++ 3 files changed, 485 insertions(+) create mode 100644 .github/skills/local-agent-sync-external-resources/references/managed-resources.yaml create mode 100644 .github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py create mode 100644 tests/github/skills/local-agent-sync-external-resources/scripts/test_manifest.py diff --git a/.github/skills/local-agent-sync-external-resources/references/managed-resources.yaml b/.github/skills/local-agent-sync-external-resources/references/managed-resources.yaml new file mode 100644 index 00000000..a3b58fa6 --- /dev/null +++ b/.github/skills/local-agent-sync-external-resources/references/managed-resources.yaml @@ -0,0 +1,220 @@ +version: 1 +sources: + github-awesome-copilot: + repository: https://github.com/github/awesome-copilot.git + ref: e986f49695491311df2774030ebe11efabd0fb77 + assets: + - upstream: skills/agentic-eval + local: .github/skills/awesome-copilot-agentic-eval + canonical_name: awesome-copilot-agentic-eval + - upstream: skills/azure-devops-cli + local: .github/skills/awesome-copilot-azure-devops-cli + canonical_name: awesome-copilot-azure-devops-cli + - upstream: skills/azure-pricing + local: .github/skills/awesome-copilot-azure-pricing + canonical_name: awesome-copilot-azure-pricing + - upstream: skills/azure-resource-health-diagnose + local: .github/skills/awesome-copilot-azure-resource-health-diagnose + canonical_name: awesome-copilot-azure-resource-health-diagnose + - upstream: skills/azure-role-selector + local: .github/skills/awesome-copilot-azure-role-selector + canonical_name: awesome-copilot-azure-role-selector + - upstream: skills/cloud-design-patterns + local: .github/skills/awesome-copilot-cloud-design-patterns + canonical_name: awesome-copilot-cloud-design-patterns + - upstream: skills/codeql + local: .github/skills/awesome-copilot-codeql + canonical_name: awesome-copilot-codeql + - upstream: skills/dependabot + local: .github/skills/awesome-copilot-dependabot + canonical_name: awesome-copilot-dependabot + - upstream: skills/secret-scanning + local: .github/skills/awesome-copilot-secret-scanning + canonical_name: awesome-copilot-secret-scanning + obra-superpowers: + repository: https://github.com/obra/superpowers.git + ref: d884ae04edebef577e82ff7c4e143debd0bbec99 + assets: + - upstream: skills/brainstorming + local: .github/skills/superpowers-brainstorming + canonical_name: superpowers-brainstorming + - upstream: skills/dispatching-parallel-agents + local: .github/skills/superpowers-dispatching-parallel-agents + canonical_name: superpowers-dispatching-parallel-agents + - upstream: skills/executing-plans + local: .github/skills/superpowers-executing-plans + canonical_name: superpowers-executing-plans + - upstream: skills/finishing-a-development-branch + local: .github/skills/superpowers-finishing-a-development-branch + canonical_name: superpowers-finishing-a-development-branch + - upstream: skills/receiving-code-review + local: .github/skills/superpowers-receiving-code-review + canonical_name: superpowers-receiving-code-review + - upstream: skills/requesting-code-review + local: .github/skills/superpowers-requesting-code-review + canonical_name: superpowers-requesting-code-review + - upstream: skills/subagent-driven-development + local: .github/skills/superpowers-subagent-driven-development + canonical_name: superpowers-subagent-driven-development + - upstream: skills/systematic-debugging + local: .github/skills/superpowers-systematic-debugging + canonical_name: superpowers-systematic-debugging + - upstream: skills/test-driven-development + local: .github/skills/superpowers-test-driven-development + canonical_name: superpowers-test-driven-development + - upstream: skills/using-git-worktrees + local: .github/skills/superpowers-using-git-worktrees + canonical_name: superpowers-using-git-worktrees + - upstream: skills/using-superpowers + local: .github/skills/superpowers-using-superpowers + canonical_name: superpowers-using-superpowers + - upstream: skills/verification-before-completion + local: .github/skills/superpowers-verification-before-completion + canonical_name: superpowers-verification-before-completion + - upstream: skills/writing-plans + local: .github/skills/superpowers-writing-plans + canonical_name: superpowers-writing-plans + hashicorp-agent-skills: + repository: https://github.com/hashicorp/agent-skills.git + ref: 339a113935812ad75c6ff90d418b739a021826d1 + assets: + - upstream: terraform/code-generation/skills/terraform-search-import + local: .github/skills/terraform-terraform-search-import + canonical_name: terraform-terraform-search-import + - upstream: terraform/code-generation/skills/terraform-test + local: .github/skills/terraform-terraform-test + canonical_name: terraform-terraform-test + mattpocock-skills: + repository: https://github.com/mattpocock/skills.git + ref: efa058a349f5ce98b6115bf8b4e0d0ef9c310e0d + assets: + - upstream: skills/engineering/code-review + local: .github/skills/mattpocock-code-review + canonical_name: mattpocock-code-review + - upstream: skills/engineering/grill-me + local: .github/skills/grill-me + canonical_name: grill-me + - upstream: skills/engineering/research + local: .github/skills/mattpocock-research + canonical_name: mattpocock-research + - upstream: skills/productivity/handoff + local: .github/skills/mattpocock-handoff + canonical_name: mattpocock-handoff + vercel-labs-skills: + repository: https://github.com/vercel-labs/skills.git + ref: 4ce6d48ac44c8b637db87b2102fea3baca719df1 + assets: + - upstream: skills/find-skills + local: .github/skills/vercel-find-skills + canonical_name: vercel-find-skills + openai-skills-curated: + repository: https://github.com/openai/skills.git + ref: 49f948faa9258a0c61caceaf225e179651397431 + assets: + - upstream: skills/.curated/gh-address-comments + local: .github/skills/openai-gh-address-comments + canonical_name: openai-gh-address-comments + - upstream: skills/.curated/gh-fix-ci + local: .github/skills/openai-gh-fix-ci + canonical_name: openai-gh-fix-ci + - upstream: skills/.curated/skill-creator + local: .github/skills/openai-skill-creator + canonical_name: openai-skill-creator + - upstream: skills/.curated/pdf + local: .github/skills/openai-pdf + canonical_name: openai-pdf + openai-skills-retained-doc: + repository: https://github.com/openai/skills.git + ref: 45d05d75363abf13f99d09e899d61e07b8010685 + assets: + - upstream: skills/.curated/doc + local: .github/skills/openai-docx + canonical_name: openai-docx + openai-skills-retained-office: + repository: https://github.com/openai/skills.git + ref: e6afb0d74cc75d220df2faf3dd6c635c2dc6a108 + assets: + - upstream: skills/.curated/spreadsheet + local: .github/skills/openai-spreadsheet + canonical_name: openai-spreadsheet + - upstream: skills/.curated/slides + local: .github/skills/openai-slides + canonical_name: openai-slides + sickn33-antigravity: + repository: https://github.com/sickn33/antigravity-awesome-skills.git + ref: 8946c6cdc8468183426d52f85054639b3e1844ae + assets: + - upstream: skills/api-design-principles + local: .github/skills/antigravity-api-design-principles + canonical_name: antigravity-api-design-principles + - upstream: skills/aws-cost-optimizer + local: .github/skills/antigravity-aws-cost-optimizer + canonical_name: antigravity-aws-cost-optimizer + - upstream: skills/cloudformation-best-practices + local: .github/skills/antigravity-cloudformation-best-practices + canonical_name: antigravity-cloudformation-best-practices + - upstream: skills/golang-pro + local: .github/skills/antigravity-golang-pro + canonical_name: antigravity-golang-pro + - upstream: skills/grafana-dashboards + local: .github/skills/antigravity-grafana-dashboards + canonical_name: antigravity-grafana-dashboards + - upstream: skills/kubernetes-architect + local: .github/skills/antigravity-kubernetes-architect + canonical_name: antigravity-kubernetes-architect + - upstream: skills/network-engineer + local: .github/skills/antigravity-network-engineer + canonical_name: antigravity-network-engineer +normalizations: + - source: obra-superpowers + from: docs/superpowers + to: tmp/superpowers +watchlist: + - source_family: github/awesome-copilot + upstream_id: azure-devops-pipelines.instructions.md + local_owner: internal-azure-devops + reason: Azure DevOps pipeline guidance was extracted into a repository-owned skill-first owner. + - source_family: github/awesome-copilot + upstream_id: go.instructions.md + local_owner: internal-go + reason: Go guidance was extracted into a repository-owned skill-first owner. + - source_family: github/awesome-copilot + upstream_id: kubernetes_manifests.instructions.md + local_owner: internal-kubernetes + reason: Kubernetes manifest guidance was extracted into repository-owned Kubernetes skills. + - source_family: github/awesome-copilot + upstream_id: shell.instructions.md + local_owner: internal-bash + reason: Shell guidance was extracted into the repository-owned Bash baseline and script skill split. + - source_family: mattpocock/skills + upstream_id: diagnose + local_owner: internal-debugging + reason: Root-cause diagnosis loop was extracted into a repository-owned owner. + - source_family: mattpocock/skills + upstream_id: tdd + local_owner: internal-tdd + reason: Test-first delivery rules were extracted into a repository-owned owner. + - source_family: mattpocock/skills + upstream_id: improve-codebase-architecture + local_owner: internal-review-high-level + reason: Architecture deepening lenses were extracted into high-level review. + - source_family: mattpocock/skills + upstream_id: zoom-out + local_owner: internal-review-high-level + reason: Codebase orientation, module maps, caller maps, and domain vocabulary were extracted into high-level review. + - source_family: mattpocock/skills + upstream_id: grill-with-docs + local_owner: internal-gateway-writing-plans + reason: Glossary and ADR side effects are not default repository conventions. + - source_family: mattpocock/skills + upstream_id: setup-matt-pocock-skills + local_owner: local-agent-sync-external-resources + reason: Setup conventions are not part of the managed source catalog. + - source_family: mattpocock/skills + upstream_id: caveman + local_owner: retired + reason: The previously retained caveman import was retired from the managed catalog by explicit user scope reduction. Do not reimport unless the user re-approves the retained import. + - source_family: addyosmani/agent-skills + upstream_id: idea-refine + local_owner: internal-gateway-idea + reason: Useful shaping frameworks and evaluation criteria were extracted into the repository-owned idea gateway; the competing autonomous workflow was retired. diff --git a/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py b/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py new file mode 100644 index 00000000..a5d93c82 --- /dev/null +++ b/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + +import yaml + + +@dataclass(frozen=True) +class ManagedAsset: + source: str + upstream: str + local: str + canonical_name: str + + +@dataclass(frozen=True) +class ManagedSource: + source_id: str + repository: str + ref: str + assets: tuple[ManagedAsset, ...] + + +@dataclass(frozen=True) +class TextReplacement: + source: str + old: str + new: str + + +@dataclass(frozen=True) +class WatchItem: + source_family: str + upstream_id: str + local_owner: str + reason: str + + +@dataclass(frozen=True) +class ManagedResources: + sources: tuple[ManagedSource, ...] + replacements: tuple[TextReplacement, ...] + watchlist: tuple[WatchItem, ...] + + @property + def assets(self) -> tuple[ManagedAsset, ...]: + return tuple(asset for source in self.sources for asset in source.assets) + + +def _require_non_empty_string(value: object, field: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{field} must be a non-empty string.") + return value.strip() + + +def load_managed_resources(path: Path) -> ManagedResources: + payload = yaml.safe_load(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict) or payload.get("version") != 1: + raise ValueError("Managed resources must be a version 1 YAML mapping.") + + raw_sources = payload.get("sources") + if not isinstance(raw_sources, dict) or not raw_sources: + raise ValueError("Managed resources must declare a non-empty sources mapping.") + + raw_normalizations = payload.get("normalizations") + if raw_normalizations is not None and not isinstance(raw_normalizations, list): + raise ValueError("normalizations must be a list.") + + raw_watchlist = payload.get("watchlist") + if not isinstance(raw_watchlist, list): + raise ValueError("watchlist must be a list.") + + seen_local_paths: set[str] = set() + seen_canonical_names: set[str] = set() + sources: list[ManagedSource] = [] + + for source_id, raw_source in raw_sources.items(): + source_id = _require_non_empty_string(source_id, "source id") + if not isinstance(raw_source, dict): + raise ValueError(f"Source {source_id} must be a mapping.") + + repository = _require_non_empty_string( + raw_source.get("repository"), f"source {source_id} repository" + ) + ref = _require_non_empty_string( + raw_source.get("ref"), f"source {source_id} ref" + ) + raw_assets = raw_source.get("assets") + if not isinstance(raw_assets, list) or not raw_assets: + raise ValueError( + f"Source {source_id} must declare a non-empty assets list." + ) + + assets: list[ManagedAsset] = [] + for raw_asset in raw_assets: + if not isinstance(raw_asset, dict): + raise ValueError( + f"Each asset in source {source_id} must be a mapping." + ) + upstream = _require_non_empty_string( + raw_asset.get("upstream"), + f"asset upstream in source {source_id}", + ) + local = _require_non_empty_string( + raw_asset.get("local"), + f"asset local in source {source_id}", + ) + canonical_name = _require_non_empty_string( + raw_asset.get("canonical_name"), + f"asset canonical_name in source {source_id}", + ) + + if local in seen_local_paths: + raise ValueError(f"duplicate local path: {local}") + seen_local_paths.add(local) + + if canonical_name in seen_canonical_names: + raise ValueError(f"duplicate canonical name: {canonical_name}") + seen_canonical_names.add(canonical_name) + + assets.append( + ManagedAsset( + source=source_id, + upstream=upstream, + local=local, + canonical_name=canonical_name, + ) + ) + + sources.append( + ManagedSource( + source_id=source_id, + repository=repository, + ref=ref, + assets=tuple(assets), + ) + ) + + replacements: list[TextReplacement] = [] + if raw_normalizations: + for raw_norm in raw_normalizations: + if not isinstance(raw_norm, dict): + raise ValueError("Each normalization must be a mapping.") + norm_source = _require_non_empty_string( + raw_norm.get("source"), "normalization source" + ) + if norm_source not in {s.source_id for s in sources}: + raise ValueError( + f"normalization source {norm_source} is not a declared source" + ) + old = _require_non_empty_string( + raw_norm.get("from"), "normalization from" + ) + new = _require_non_empty_string( + raw_norm.get("to"), "normalization to" + ) + replacements.append( + TextReplacement(source=norm_source, old=old, new=new) + ) + + watchlist: list[WatchItem] = [] + for raw_item in raw_watchlist: + if not isinstance(raw_item, dict): + raise ValueError("Each watchlist item must be a mapping.") + source_family = _require_non_empty_string( + raw_item.get("source_family"), "watchlist source_family" + ) + upstream_id = _require_non_empty_string( + raw_item.get("upstream_id"), "watchlist upstream_id" + ) + local_owner = _require_non_empty_string( + raw_item.get("local_owner"), "watchlist local_owner" + ) + reason = _require_non_empty_string( + raw_item.get("reason"), "watchlist reason" + ) + watchlist.append( + WatchItem( + source_family=source_family, + upstream_id=upstream_id, + local_owner=local_owner, + reason=reason, + ) + ) + + return ManagedResources( + sources=tuple(sources), + replacements=tuple(replacements), + watchlist=tuple(watchlist), + ) diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_manifest.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_manifest.py new file mode 100644 index 00000000..9371a713 --- /dev/null +++ b/tests/github/skills/local-agent-sync-external-resources/scripts/test_manifest.py @@ -0,0 +1,74 @@ +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = next( + parent for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +SCRIPT_DIR = ( + REPO_ROOT / ".github/skills/local-agent-sync-external-resources/scripts" +) +sys.path.insert(0, SCRIPT_DIR.as_posix()) + +from sync_external_resources_core import ( # noqa: E402 + load_managed_resources, +) + + +@pytest.fixture +def repo_root() -> Path: + return REPO_ROOT + + +def test_live_manifest_preserves_declared_scope(repo_root: Path) -> None: + manifest = load_managed_resources( + repo_root + / ".github/skills/local-agent-sync-external-resources/references/managed-resources.yaml" + ) + + assert len(manifest.assets) == 43 + assert len(manifest.watchlist) == 12 + assert { + item.local for item in manifest.assets if item.source == "obra-superpowers" + } == { + ".github/skills/superpowers-brainstorming", + ".github/skills/superpowers-dispatching-parallel-agents", + ".github/skills/superpowers-executing-plans", + ".github/skills/superpowers-finishing-a-development-branch", + ".github/skills/superpowers-receiving-code-review", + ".github/skills/superpowers-requesting-code-review", + ".github/skills/superpowers-subagent-driven-development", + ".github/skills/superpowers-systematic-debugging", + ".github/skills/superpowers-test-driven-development", + ".github/skills/superpowers-using-git-worktrees", + ".github/skills/superpowers-using-superpowers", + ".github/skills/superpowers-verification-before-completion", + ".github/skills/superpowers-writing-plans", + } + + +def test_manifest_rejects_duplicate_local_paths(tmp_path: Path) -> None: + path = tmp_path / "managed-resources.yaml" + path.write_text( + """\ +version: 1 +sources: + source: + repository: https://github.com/example/repo.git + ref: abc123 + assets: + - upstream: skills/one + local: .github/skills/example + canonical_name: example + - upstream: skills/two + local: .github/skills/example + canonical_name: example-two +watchlist: [] +""", + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="duplicate local path"): + load_managed_resources(path) From 3a9db4815142013145b907293f21fba3e419f46f Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 12:47:18 +0200 Subject: [PATCH 31/59] feat: stage external resource candidates safely --- .../scripts/sync_external_resources_core.py | 122 ++++++++++++ .../scripts/test_candidate.py | 182 ++++++++++++++++++ 2 files changed, 304 insertions(+) create mode 100644 tests/github/skills/local-agent-sync-external-resources/scripts/test_candidate.py diff --git a/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py b/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py index a5d93c82..663d51d5 100644 --- a/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py +++ b/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py @@ -1,5 +1,8 @@ from __future__ import annotations +import re +import shutil +import subprocess from dataclasses import dataclass from pathlib import Path @@ -189,3 +192,122 @@ def load_managed_resources(path: Path) -> ManagedResources: replacements=tuple(replacements), watchlist=tuple(watchlist), ) + + +class SyncCommandError(Exception): + def __init__(self, command: list[str], exit_code: int, stderr: str) -> None: + self.command = command + self.exit_code = exit_code + self.stderr = stderr + super().__init__( + f"Command {command!r} exited {exit_code}: {stderr.strip()[:500]}" + ) + + +def _run_command(command: list[str], cwd: Path | None = None) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + command, + cwd=cwd, + check=False, + capture_output=True, + text=True, + timeout=60, + ) + if result.returncode != 0: + raise SyncCommandError(command, result.returncode, result.stderr) + return result + + +def _run_git(repo_root: Path, args: list[str]) -> subprocess.CompletedProcess[str]: + return _run_command(["git", *args], cwd=repo_root) + + +def validate_external_workspace(repo_root: Path, workspace: Path) -> None: + repo = repo_root.resolve() + external = workspace.resolve() + if external == repo or external.is_relative_to(repo): + raise ValueError( + f"External refresh workspace must be outside the repository: {workspace}" + ) + + +def find_dirty_targets( + repo_root: Path, + assets: tuple[ManagedAsset, ...], +) -> tuple[str, ...]: + if not assets: + return () + result = _run_git( + repo_root, + ["status", "--porcelain=v1", "--", *(asset.local for asset in assets)], + ) + return tuple( + line[3:] + for line in result.stdout.splitlines() + if len(line) > 3 + ) + + +def materialize_candidate( + resources: ManagedResources, + workspace: Path, + candidate: Path, +) -> None: + if candidate.exists(): + shutil.rmtree(candidate) + candidate.mkdir(parents=True) + + for source in resources.sources: + source_root = workspace / "sources" / source.source_id + for asset in source.assets: + upstream_path = source_root / asset.upstream + if not upstream_path.exists(): + raise ValueError( + f"Missing upstream path: {asset.upstream} in source {source.source_id}" + ) + target_path = candidate / asset.local + target_path.parent.mkdir(parents=True, exist_ok=True) + if upstream_path.is_dir(): + shutil.copytree(upstream_path, target_path) + else: + shutil.copy2(upstream_path, target_path) + + +_FRONTMATTER_NAME_RE = re.compile(r"^(name\s*:\s*).*$", re.MULTILINE) + + +def normalize_candidate( + resources: ManagedResources, + candidate: Path, +) -> tuple[str, ...]: + replacements_by_source: dict[str, list[TextReplacement]] = {} + for replacement in resources.replacements: + replacements_by_source.setdefault(replacement.source, []).append(replacement) + + changed: list[str] = [] + for asset in resources.assets: + asset_dir = candidate / asset.local + if not asset_dir.exists(): + continue + for file_path in sorted(asset_dir.rglob("*")): + if not file_path.is_file(): + continue + try: + content = file_path.read_text(encoding="utf-8") + except (UnicodeDecodeError, ValueError): + continue + + original = content + if file_path.suffix in {".md", ".yaml", ".yml"}: + content = _FRONTMATTER_NAME_RE.sub( + rf"\g<1>{asset.canonical_name}", content, count=1 + ) + + for replacement in replacements_by_source.get(asset.source, []): + content = content.replace(replacement.old, replacement.new) + + if content != original: + file_path.write_text(content, encoding="utf-8") + changed.append(file_path.relative_to(candidate).as_posix()) + + return tuple(sorted(changed)) diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_candidate.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_candidate.py new file mode 100644 index 00000000..c2488686 --- /dev/null +++ b/tests/github/skills/local-agent-sync-external-resources/scripts/test_candidate.py @@ -0,0 +1,182 @@ +import shutil +import subprocess +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = next( + parent for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +SCRIPT_DIR = ( + REPO_ROOT / ".github/skills/local-agent-sync-external-resources/scripts" +) +sys.path.insert(0, SCRIPT_DIR.as_posix()) + +from sync_external_resources_core import ( # noqa: E402 + ManagedAsset, + ManagedResources, + ManagedSource, + TextReplacement, + find_dirty_targets, + materialize_candidate, + normalize_candidate, + validate_external_workspace, +) + + +def _run_git(cwd: Path, args: list[str]) -> None: + subprocess.run( + ["git", *args], + cwd=cwd, + check=True, + capture_output=True, + text=True, + ) + + +def _commit_all(repo: Path) -> None: + _run_git(repo, ["add", "-A"]) + _run_git( + repo, + ["commit", "-m", "snapshot", "--allow-empty"], + ) + + +@pytest.fixture +def git_repo(tmp_path: Path) -> Path: + repo = tmp_path / "repo" + repo.mkdir() + _run_git(repo, ["init"]) + _run_git(repo, ["config", "user.email", "test@test.com"]) + _run_git(repo, ["config", "user.name", "Test"]) + (repo / "README.md").write_text("init\n", encoding="utf-8") + _commit_all(repo) + return repo + + +def _example_asset(local: str = ".github/skills/example") -> ManagedAsset: + return ManagedAsset( + source="test", + upstream="skills/example", + local=local, + canonical_name="example", + ) + + +def _superpowers_resources() -> ManagedResources: + asset = ManagedAsset( + source="obra-superpowers", + upstream="skills/brainstorming", + local=".github/skills/superpowers-brainstorming", + canonical_name="superpowers-brainstorming", + ) + source = ManagedSource( + source_id="obra-superpowers", + repository="https://github.com/obra/superpowers.git", + ref="abc123", + assets=(asset,), + ) + replacement = TextReplacement( + source="obra-superpowers", + old="docs/superpowers", + new="tmp/superpowers", + ) + return ManagedResources( + sources=(source,), + replacements=(replacement,), + watchlist=(), + ) + + +def test_workspace_inside_repository_is_rejected(tmp_path: Path) -> None: + repo = tmp_path / "repo" + workspace = repo / "tmp" / "refresh" + workspace.mkdir(parents=True) + + with pytest.raises(ValueError, match="outside the repository"): + validate_external_workspace(repo, workspace) + + +def test_workspace_outside_repository_is_accepted(tmp_path: Path) -> None: + repo = tmp_path / "repo" + repo.mkdir() + workspace = tmp_path / "external-workspace" + workspace.mkdir() + + validate_external_workspace(repo, workspace) + + +def test_dirty_managed_target_is_reported(git_repo: Path) -> None: + target = git_repo / ".github/skills/example/SKILL.md" + target.parent.mkdir(parents=True) + target.write_text("---\nname: example\n---\n", encoding="utf-8") + _commit_all(git_repo) + target.write_text("---\nname: locally-edited\n---\n", encoding="utf-8") + + assert find_dirty_targets(git_repo, (_example_asset(),)) == ( + ".github/skills/example/SKILL.md", + ) + + +def test_clean_managed_target_is_not_reported(git_repo: Path) -> None: + target = git_repo / ".github/skills/example/SKILL.md" + target.parent.mkdir(parents=True) + target.write_text("---\nname: example\n---\n", encoding="utf-8") + _commit_all(git_repo) + + assert find_dirty_targets(git_repo, (_example_asset(),)) == () + + +def test_normalization_updates_name_and_declared_text_only( + tmp_path: Path, +) -> None: + candidate = tmp_path / "candidate" + skill = candidate / ".github/skills/superpowers-brainstorming/SKILL.md" + skill.parent.mkdir(parents=True) + skill.write_text( + "---\nname: brainstorming\n---\nWrite to docs/superpowers.\n", + encoding="utf-8", + ) + + changed = normalize_candidate(_superpowers_resources(), candidate) + + assert changed == (".github/skills/superpowers-brainstorming/SKILL.md",) + content = skill.read_text(encoding="utf-8") + assert "name: superpowers-brainstorming" in content + assert "tmp/superpowers" in content + + +def test_materialize_candidate_copies_upstream_to_local( + tmp_path: Path, +) -> None: + workspace = tmp_path / "workspace" + source_checkout = workspace / "sources" / "test-source" / "skills" / "example" + source_checkout.mkdir(parents=True) + (source_checkout / "SKILL.md").write_text( + "---\nname: upstream-name\n---\n", encoding="utf-8" + ) + + asset = ManagedAsset( + source="test-source", + upstream="skills/example", + local=".github/skills/example", + canonical_name="example", + ) + source = ManagedSource( + source_id="test-source", + repository="https://example.com/repo.git", + ref="abc", + assets=(asset,), + ) + resources = ManagedResources( + sources=(source,), replacements=(), watchlist=() + ) + + candidate = tmp_path / "candidate" + materialize_candidate(resources, workspace, candidate) + + target = candidate / ".github/skills/example/SKILL.md" + assert target.exists() + assert "upstream-name" in target.read_text(encoding="utf-8") From 980d4d03f44d861aa7d687b4b408d9c77f3a111e Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 13:14:07 +0200 Subject: [PATCH 32/59] fix: verify external override replay atomically --- .../scripts/sync_external_resources_core.py | 190 ++++++++++++++++++ .../scripts/test_candidate.py | 131 ++++++++++++ 2 files changed, 321 insertions(+) diff --git a/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py b/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py index 663d51d5..271cc0cb 100644 --- a/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py +++ b/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py @@ -1,10 +1,13 @@ from __future__ import annotations +import hashlib import re import shutil import subprocess +import tempfile from dataclasses import dataclass from pathlib import Path +from typing import Literal import yaml @@ -311,3 +314,190 @@ def normalize_candidate( changed.append(file_path.relative_to(candidate).as_posix()) return tuple(sorted(changed)) + + +@dataclass(frozen=True) +class ImportedOverride: + override_id: str + target_path: str + patch_path: str + apply_strategy: str + expected_content_hash: str + + +@dataclass(frozen=True) +class OverrideResult: + override_id: str + status: Literal["applied", "already-applied"] + target_path: str + + +def load_overrides(path: Path) -> tuple[ImportedOverride, ...]: + payload = yaml.safe_load(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict) or payload.get("version") != 1: + raise ValueError("Override registry must be a version 1 YAML mapping.") + + raw_overrides = payload.get("overrides") + if not isinstance(raw_overrides, list): + raise ValueError("Override registry must declare an overrides list.") + + overrides: list[ImportedOverride] = [] + for entry in raw_overrides: + if not isinstance(entry, dict): + raise ValueError("Each override entry must be a mapping.") + overrides.append( + ImportedOverride( + override_id=_require_non_empty_string( + entry.get("id"), "override id" + ), + target_path=_require_non_empty_string( + entry.get("target_path"), "override target_path" + ), + patch_path=_require_non_empty_string( + entry.get("patch_path"), "override patch_path" + ), + apply_strategy=_require_non_empty_string( + entry.get("apply_strategy"), "override apply_strategy" + ), + expected_content_hash=_require_non_empty_string( + entry.get("expected_content_hash"), + "override expected_content_hash", + ), + ) + ) + return tuple(overrides) + + +def select_overrides( + overrides: tuple[ImportedOverride, ...], + requested_ids: tuple[str, ...], +) -> tuple[ImportedOverride, ...]: + by_id = {o.override_id: o for o in overrides} + selected: list[ImportedOverride] = [] + for rid in requested_ids: + if rid not in by_id: + raise ValueError(f"unknown override id: {rid}") + selected.append(by_id[rid]) + return tuple(selected) + + +def _sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as fh: + for chunk in iter(lambda: fh.read(65536), b""): + digest.update(chunk) + return digest.hexdigest() + + +def verify_override_hash( + candidate_repo: Path, override: ImportedOverride +) -> None: + target = candidate_repo / override.target_path + if not target.exists(): + raise ValueError( + f"Override target missing on disk: {override.target_path}" + ) + actual = _sha256_file(target) + if actual != override.expected_content_hash: + raise ValueError( + f"content hash mismatch for {override.target_path}: " + f"expected {override.expected_content_hash}, got {actual}" + ) + + +def _replay_one_override( + trial_repo: Path, + override: ImportedOverride, + patches_root: Path, +) -> OverrideResult: + patch_file = patches_root / override.patch_path + if not patch_file.exists(): + raise ValueError(f"Override patch missing: {override.patch_path}") + + patch_text = patch_file.read_text(encoding="utf-8") + target = trial_repo / override.target_path + before_content = target.read_text(encoding="utf-8") if target.exists() else "" + + check_result = subprocess.run( + ["git", "apply", "--check", "--", str(patch_file)], + cwd=trial_repo, + capture_output=True, + text=True, + check=False, + ) + + if check_result.returncode == 0: + _run_command(["git", "apply", "--", str(patch_file)], cwd=trial_repo) + after_content = target.read_text(encoding="utf-8") + if after_content == before_content: + return OverrideResult( + override_id=override.override_id, + status="already-applied", + target_path=override.target_path, + ) + return OverrideResult( + override_id=override.override_id, + status="applied", + target_path=override.target_path, + ) + + if override.apply_strategy == "git-apply-3way": + check_3way = subprocess.run( + ["git", "apply", "--3way", "--check", "--", str(patch_file)], + cwd=trial_repo, + capture_output=True, + text=True, + check=False, + ) + if check_3way.returncode == 0: + _run_command( + ["git", "apply", "--3way", "--", str(patch_file)], + cwd=trial_repo, + ) + return OverrideResult( + override_id=override.override_id, + status="applied", + target_path=override.target_path, + ) + + raise ValueError( + f"Override {override.override_id} patch does not apply cleanly" + ) + + +def _replace_tree_contents(dest: Path, source: Path) -> None: + for item in dest.iterdir(): + if item.name == ".git": + continue + if item.is_dir(): + shutil.rmtree(item) + else: + item.unlink() + for item in source.iterdir(): + if item.name == ".git": + continue + target = dest / item.name + if item.is_dir(): + shutil.copytree(item, target) + else: + shutil.copy2(item, target) + + +def replay_overrides( + candidate_repo: Path, + overrides: tuple[ImportedOverride, ...], + patches_root: Path | None = None, +) -> tuple[OverrideResult, ...]: + if patches_root is None: + patches_root = candidate_repo + with tempfile.TemporaryDirectory(prefix="external-override-") as raw_trial: + trial = Path(raw_trial) / "candidate" + shutil.copytree(candidate_repo, trial) + results: list[OverrideResult] = [] + for item in overrides: + result = _replay_one_override(trial, item, patches_root) + results.append(result) + for item in overrides: + verify_override_hash(trial, item) + _replace_tree_contents(candidate_repo, trial) + return tuple(results) diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_candidate.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_candidate.py index c2488686..5be6d3a8 100644 --- a/tests/github/skills/local-agent-sync-external-resources/scripts/test_candidate.py +++ b/tests/github/skills/local-agent-sync-external-resources/scripts/test_candidate.py @@ -1,3 +1,4 @@ +import hashlib import shutil import subprocess import sys @@ -15,14 +16,19 @@ sys.path.insert(0, SCRIPT_DIR.as_posix()) from sync_external_resources_core import ( # noqa: E402 + ImportedOverride, ManagedAsset, ManagedResources, ManagedSource, TextReplacement, find_dirty_targets, + load_overrides, materialize_candidate, normalize_candidate, + replay_overrides, + select_overrides, validate_external_workspace, + verify_override_hash, ) @@ -180,3 +186,128 @@ def test_materialize_candidate_copies_upstream_to_local( target = candidate / ".github/skills/example/SKILL.md" assert target.exists() assert "upstream-name" in target.read_text(encoding="utf-8") + + +@pytest.fixture +def candidate_repo(tmp_path: Path) -> Path: + repo = tmp_path / "candidate" + repo.mkdir() + _run_git(repo, ["init"]) + _run_git(repo, ["config", "user.email", "test@test.com"]) + _run_git(repo, ["config", "user.name", "Test"]) + (repo / "README.md").write_text("init\n", encoding="utf-8") + _commit_all(repo) + return repo + + +def _sha256(content: str) -> str: + return hashlib.sha256(content.encode("utf-8")).hexdigest() + + +def _make_override( + candidate_repo: Path, + override_id: str = "test-override", + target_rel: str = ".github/skills/test/SKILL.md", + original_content: str = "---\nname: test\n---\nOriginal content.\n", + patched_content: str = "---\nname: test\n---\nPatched content.\n", +) -> tuple[ImportedOverride, Path, Path]: + target = candidate_repo / target_rel + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(original_content, encoding="utf-8") + _commit_all(candidate_repo) + + patch_dir = candidate_repo / "patches" + patch_dir.mkdir(exist_ok=True) + patch_path = patch_dir / f"{override_id}.patch" + + target.write_text(patched_content, encoding="utf-8") + result = subprocess.run( + ["git", "diff", "--", target_rel], + cwd=candidate_repo, + capture_output=True, + text=True, + check=True, + ) + patch_path.write_text(result.stdout, encoding="utf-8") + target.write_text(original_content, encoding="utf-8") + + override = ImportedOverride( + override_id=override_id, + target_path=target_rel, + patch_path=patch_path.relative_to(candidate_repo).as_posix(), + apply_strategy="git-apply", + expected_content_hash=_sha256(patched_content), + ) + return override, target, patch_path + + +def test_override_applies_cleanly(candidate_repo: Path) -> None: + override, target, _ = _make_override(candidate_repo) + + results = replay_overrides(candidate_repo, (override,)) + + assert len(results) == 1 + assert results[0].override_id == "test-override" + assert results[0].status == "applied" + assert "Patched content." in target.read_text(encoding="utf-8") + + +def test_conflicting_second_override_leaves_candidate_unchanged( + candidate_repo: Path, +) -> None: + first, target, _ = _make_override( + candidate_repo, + override_id="first", + patched_content="---\nname: test\n---\nFirst patch.\n", + ) + second = ImportedOverride( + override_id="second", + target_path=first.target_path, + patch_path=first.patch_path, + apply_strategy="git-apply", + expected_content_hash="x" * 64, + ) + before_content = target.read_text(encoding="utf-8") + + with pytest.raises(ValueError): + replay_overrides(candidate_repo, (first, second)) + + after_content = target.read_text(encoding="utf-8") + assert before_content == after_content + + +def test_unknown_requested_override_id_is_rejected() -> None: + overrides = ( + ImportedOverride( + override_id="known", + target_path=".github/skills/test/SKILL.md", + patch_path="patches/test.patch", + apply_strategy="git-apply", + expected_content_hash="a" * 64, + ), + ) + + with pytest.raises(ValueError, match="unknown override id: missing"): + select_overrides(overrides, ("missing",)) + + +def test_load_overrides_from_yaml(tmp_path: Path) -> None: + registry = tmp_path / "overrides.yaml" + registry.write_text( + """\ +version: 1 +overrides: + - id: test-override + target_path: .github/skills/test/SKILL.md + patch_path: patches/test.patch + apply_strategy: git-apply + expected_content_hash: abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 +""", + encoding="utf-8", + ) + + overrides = load_overrides(registry) + + assert len(overrides) == 1 + assert overrides[0].override_id == "test-override" + assert overrides[0].expected_content_hash == "abcdef0123456789" * 4 From 0c7c8b5420b4fd6c01f57e180e7d6fd1ca66aeb3 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 13:16:06 +0200 Subject: [PATCH 33/59] feat: add manifest-driven external resource sync --- .../scripts/sync_external_resources.py | 303 ++++++++++++++++++ .../scripts/test_cli.py | 165 ++++++++++ 2 files changed, 468 insertions(+) create mode 100644 .github/skills/local-agent-sync-external-resources/scripts/sync_external_resources.py create mode 100644 tests/github/skills/local-agent-sync-external-resources/scripts/test_cli.py diff --git a/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources.py b/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources.py new file mode 100644 index 00000000..4d921cb0 --- /dev/null +++ b/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources.py @@ -0,0 +1,303 @@ +"""Manifest-driven CLI for auditing, planning, and applying external resource syncs.""" +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from dataclasses import dataclass, field +from pathlib import Path +from typing import Literal, Sequence + +SCRIPT_DIR = Path(__file__).resolve().parent +sys.path.insert(0, SCRIPT_DIR.as_posix()) + +from sync_external_resources_core import ( # noqa: E402 + ManagedResources, + OverrideResult, + SyncCommandError, + find_dirty_targets, + load_managed_resources, + load_overrides, + materialize_candidate, + normalize_candidate, + replay_overrides, + validate_external_workspace, +) + +DEFAULT_MANIFEST = ( + SCRIPT_DIR.parent / "references" / "managed-resources.yaml" +).as_posix() +DEFAULT_OVERRIDES = ( + SCRIPT_DIR.parent / "references" / "imported-asset-overrides.yaml" +).as_posix() + + +@dataclass(frozen=True) +class SyncOutcome: + mode: Literal["audit", "plan", "apply"] + workspace: str | None + managed_assets: int + changed_paths: tuple[str, ...] + override_results: tuple[OverrideResult, ...] + validations: tuple[str, ...] + blockers: tuple[str, ...] + repository_changed: bool + + def to_dict(self) -> dict[str, object]: + return { + "mode": self.mode, + "workspace": self.workspace, + "managed_assets": self.managed_assets, + "changed_paths": list(self.changed_paths), + "override_results": [ + { + "override_id": r.override_id, + "status": r.status, + "target_path": r.target_path, + } + for r in self.override_results + ], + "validations": list(self.validations), + "blockers": list(self.blockers), + "repository_changed": self.repository_changed, + } + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + description="Audit, plan, or apply declared external resource refreshes." + ) + parser.add_argument("mode", choices=("audit", "plan", "apply")) + parser.add_argument("--repo-root", default=".") + parser.add_argument("--workspace") + parser.add_argument("--manifest", default=DEFAULT_MANIFEST) + parser.add_argument("--overrides", default=DEFAULT_OVERRIDES) + parser.add_argument( + "--source-root", + help="Use prepared source checkouts instead of network fetch.", + ) + parser.add_argument("--allow-dirty", action="store_true") + parser.add_argument("--format", choices=("text", "json"), default="text") + return parser + + +def _run_git(repo_root: Path, args: list[str]) -> subprocess.CompletedProcess[str]: + result = subprocess.run( + ["git", *args], + cwd=repo_root, + check=False, + capture_output=True, + text=True, + timeout=60, + ) + if result.returncode != 0: + raise SyncCommandError(["git", *args], result.returncode, result.stderr) + return result + + +def _build_candidate_patch( + repo_root: Path, candidate: Path, resources: ManagedResources +) -> str: + diff_result = subprocess.run( + ["git", "diff", "--no-index", "--", "."], + cwd=candidate, + check=False, + capture_output=True, + text=True, + ) + return diff_result.stdout + + +def _apply_candidate_patch(repo_root: Path, patch_text: str) -> bool: + if not patch_text.strip(): + return False + result = subprocess.run( + ["git", "apply", "--check", "-"], + cwd=repo_root, + input=patch_text, + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + raise ValueError(f"Patch check failed: {result.stderr[:500]}") + subprocess.run( + ["git", "apply", "-"], + cwd=repo_root, + input=patch_text, + check=True, + capture_output=True, + text=True, + ) + return True + + +def _audit( + repo_root: Path, + resources: ManagedResources, + overrides_path: Path, +) -> SyncOutcome: + blockers: list[str] = [] + dirty = find_dirty_targets(repo_root, resources.assets) + if dirty: + blockers.append(f"dirty managed targets: {', '.join(dirty)}") + + overrides = load_overrides(overrides_path) if overrides_path.exists() else () + + return SyncOutcome( + mode="audit", + workspace=None, + managed_assets=len(resources.assets), + changed_paths=(), + override_results=(), + validations=("manifest-parsed", "overrides-parsed"), + blockers=tuple(blockers), + repository_changed=False, + ) + + +def _plan( + repo_root: Path, + workspace: Path, + resources: ManagedResources, + overrides_path: Path, + source_root: Path | None, +) -> SyncOutcome: + validate_external_workspace(repo_root, workspace) + + candidate = workspace / "candidate" + materialize_candidate(resources, workspace, candidate) + changed = normalize_candidate(resources, candidate) + + overrides = load_overrides(overrides_path) if overrides_path.exists() else () + override_results = replay_overrides(candidate, overrides, overrides_path.parent.parent) if overrides else () + + return SyncOutcome( + mode="plan", + workspace=str(workspace), + managed_assets=len(resources.assets), + changed_paths=tuple(changed), + override_results=override_results, + validations=("candidate-built", "normalized", "overrides-replayed"), + blockers=(), + repository_changed=False, + ) + + +def _apply( + repo_root: Path, + workspace: Path, + resources: ManagedResources, + overrides_path: Path, + source_root: Path | None, + allow_dirty: bool, +) -> SyncOutcome: + validate_external_workspace(repo_root, workspace) + + dirty = find_dirty_targets(repo_root, resources.assets) + if dirty and not allow_dirty: + return SyncOutcome( + mode="apply", + workspace=str(workspace), + managed_assets=len(resources.assets), + changed_paths=(), + override_results=(), + validations=(), + blockers=(f"dirty managed targets: {', '.join(dirty)}",), + repository_changed=False, + ) + + candidate = workspace / "candidate" + materialize_candidate(resources, workspace, candidate) + changed = normalize_candidate(resources, candidate) + + overrides = load_overrides(overrides_path) if overrides_path.exists() else () + override_results = replay_overrides(candidate, overrides, overrides_path.parent.parent) if overrides else () + + patch_text = _build_candidate_patch(repo_root, candidate, resources) + repository_changed = _apply_candidate_patch(repo_root, patch_text) + + return SyncOutcome( + mode="apply", + workspace=str(workspace), + managed_assets=len(resources.assets), + changed_paths=tuple(changed), + override_results=override_results, + validations=("candidate-built", "normalized", "overrides-replayed", "patch-applied"), + blockers=(), + repository_changed=repository_changed, + ) + + +def _format_text(outcome: SyncOutcome) -> str: + lines = [ + f"Mode: {outcome.mode}", + f"Workspace: {outcome.workspace or 'n/a'}", + f"Managed assets: {outcome.managed_assets}", + f"Changed paths: {len(outcome.changed_paths)}", + f"Override results: {len(outcome.override_results)}", + f"Validations: {', '.join(outcome.validations) or 'none'}", + f"Repository changed: {outcome.repository_changed}", + ] + if outcome.blockers: + lines.append(f"Blockers: {'; '.join(outcome.blockers)}") + return "\n".join(lines) + + +def run(argv: Sequence[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + + repo_root = Path(args.repo_root).resolve() + manifest_path = Path(args.manifest) + if not manifest_path.is_absolute(): + manifest_path = repo_root / manifest_path + overrides_path = Path(args.overrides) + if not overrides_path.is_absolute(): + overrides_path = repo_root / overrides_path + + resources = load_managed_resources(manifest_path) + + if args.mode == "audit": + outcome = _audit(repo_root, resources, overrides_path) + elif args.mode == "plan": + if not args.workspace: + parser.error("plan mode requires --workspace") + outcome = _plan( + repo_root, + Path(args.workspace).resolve(), + resources, + overrides_path, + Path(args.source_root).resolve() if args.source_root else None, + ) + elif args.mode == "apply": + if not args.workspace: + parser.error("apply mode requires --workspace") + outcome = _apply( + repo_root, + Path(args.workspace).resolve(), + resources, + overrides_path, + Path(args.source_root).resolve() if args.source_root else None, + args.allow_dirty, + ) + else: + parser.error(f"unknown mode: {args.mode}") + + if args.format == "json": + print(json.dumps(outcome.to_dict(), indent=2)) + else: + print(_format_text(outcome)) + + if outcome.blockers: + return 2 + return 0 + + +def main(argv: Sequence[str] | None = None) -> int: + return run(argv) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_cli.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_cli.py new file mode 100644 index 00000000..f25ba504 --- /dev/null +++ b/tests/github/skills/local-agent-sync-external-resources/scripts/test_cli.py @@ -0,0 +1,165 @@ +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = next( + parent for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +SCRIPT_DIR = ( + REPO_ROOT / ".github/skills/local-agent-sync-external-resources/scripts" +) +sys.path.insert(0, SCRIPT_DIR.as_posix()) + +from sync_external_resources import main # noqa: E402 +from sync_external_resources_core import ( # noqa: E402 + load_managed_resources, + load_overrides, +) + + +def _run_git(cwd: Path, args: list[str]) -> None: + subprocess.run( + ["git", *args], + cwd=cwd, + check=True, + capture_output=True, + text=True, + ) + + +def _commit_all(repo: Path) -> None: + _run_git(repo, ["add", "-A"]) + _run_git(repo, ["commit", "-m", "snapshot", "--allow-empty"]) + + +@pytest.fixture +def repo_root() -> Path: + return REPO_ROOT + + +def test_audit_does_not_fetch_or_write(repo_root: Path) -> None: + result = subprocess.run( + [ + sys.executable, + str(SCRIPT_DIR / "sync_external_resources.py"), + "audit", + "--repo-root", + str(repo_root), + "--format", + "json", + ], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 0 + payload = json.loads(result.stdout) + assert payload["mode"] == "audit" + assert payload["repository_changed"] is False + assert payload["managed_assets"] == 43 + + +def test_apply_refuses_dirty_target(tmp_path: Path) -> None: + repo = tmp_path / "repo" + repo.mkdir() + _run_git(repo, ["init"]) + _run_git(repo, ["config", "user.email", "test@test.com"]) + _run_git(repo, ["config", "user.name", "Test"]) + + manifest_src = REPO_ROOT / ".github/skills/local-agent-sync-external-resources/references/managed-resources.yaml" + overrides_src = REPO_ROOT / ".github/skills/local-agent-sync-external-resources/references/imported-asset-overrides.yaml" + + mini_manifest = tmp_path / "manifest.yaml" + mini_manifest.write_text( + """\ +version: 1 +sources: + test-source: + repository: https://example.com/repo.git + ref: abc123 + assets: + - upstream: skills/example + local: .github/skills/example + canonical_name: example +watchlist: [] +""", + encoding="utf-8", + ) + + mini_overrides = tmp_path / "overrides.yaml" + mini_overrides.write_text( + """\ +version: 1 +overrides: [] +""", + encoding="utf-8", + ) + + target = repo / ".github/skills/example/SKILL.md" + target.parent.mkdir(parents=True) + target.write_text("---\nname: example\n---\n", encoding="utf-8") + _commit_all(repo) + target.write_text("---\nname: locally-edited\n---\n", encoding="utf-8") + + workspace = tmp_path / "external-workspace" + workspace.mkdir() + + result = subprocess.run( + [ + sys.executable, + str(SCRIPT_DIR / "sync_external_resources.py"), + "apply", + "--repo-root", + str(repo), + "--workspace", + str(workspace), + "--manifest", + str(mini_manifest), + "--overrides", + str(mini_overrides), + ], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 2 + + +def test_bundle_exposes_one_public_cli(repo_root: Path) -> None: + scripts = repo_root / ".github/skills/local-agent-sync-external-resources/scripts" + public_scripts = sorted( + path.name + for path in scripts.glob("*.py") + if not path.name.endswith("_core.py") + ) + + assert public_scripts == ["sync_external_resources.py"] + + +def test_agent_and_skill_do_not_route_to_unneeded_skills(repo_root: Path) -> None: + paths = ( + repo_root / ".github/agents/local-sync-external-resources.agent.md", + repo_root / ".github/skills/local-agent-sync-external-resources/SKILL.md", + ) + text = "\n".join( + path.read_text(encoding="utf-8") for path in paths if path.exists() + ) + + if not text: + pytest.skip("Agent or skill file not yet rewritten") + + for forbidden in ( + "openai-skill-creator", + "internal-skill-creator", + "internal-agent-creator", + "internal-gateway-idea", + "internal-copilot-docs-research", + "internal-copilot-audit", + ): + assert forbidden not in text From d8390a4c714fe4ae8900ea610b03177fb97bfd6f Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 13:19:01 +0200 Subject: [PATCH 34/59] refactor: validate consolidated external resource manifest --- .github/scripts/lib/catalog_checks.py | 241 ++++++++---------- .github/scripts/lib/shared.py | 2 +- ...test_external_resource_catalog_contract.py | 104 ++++++++ 3 files changed, 213 insertions(+), 134 deletions(-) create mode 100644 tests/github/scripts/lib/test_external_resource_catalog_contract.py diff --git a/.github/scripts/lib/catalog_checks.py b/.github/scripts/lib/catalog_checks.py index 933deef9..24142d83 100644 --- a/.github/scripts/lib/catalog_checks.py +++ b/.github/scripts/lib/catalog_checks.py @@ -13,7 +13,7 @@ IMPORTED_ASSET_OVERRIDES_PATH, INVENTORY_PATH, LEGACY_AGENT_TOOL_IDS, - SUPERPOWERS_NORMALIZATION_PATH, + MANAGED_EXTERNAL_RESOURCES_PATH, Finding, finding_sort_key, is_imported_asset, @@ -55,6 +55,7 @@ def run_consistency_checks(root: Path, include_token_risks: bool = False) -> lis findings.extend(check_source_instruction_contracts(root)) findings.extend(check_residual_instruction_family_references(root)) findings.extend(check_imported_asset_overrides(root)) + findings.extend(check_external_resource_manifest(root)) findings.extend(check_superpowers_import_naming(root)) findings.extend(check_broken_local_links(root)) if include_token_risks: @@ -742,183 +743,157 @@ def check_imported_asset_overrides(root: Path) -> list[Finding]: return findings -def check_superpowers_import_naming(root: Path) -> list[Finding]: - config_path = root / SUPERPOWERS_NORMALIZATION_PATH - if not config_path.exists(): +def check_external_resource_manifest(root: Path) -> list[Finding]: + manifest_path = root / MANAGED_EXTERNAL_RESOURCES_PATH + if not manifest_path.exists(): return [ Finding( severity="blocking", - code="superpowers-normalization-reference-missing", - path=SUPERPOWERS_NORMALIZATION_PATH, - message="The obra/superpowers import normalization reference is missing.", - suggestion="Restore the reference so sync refreshes and catalog validation share one naming map.", + code="external-resource-manifest-missing", + path=MANAGED_EXTERNAL_RESOURCES_PATH, + message="The consolidated external resource manifest is missing.", + suggestion="Restore references/managed-resources.yaml in the sync bundle.", ) ] try: - payload = yaml.safe_load(read_text(config_path)) or {} + payload = yaml.safe_load(read_text(manifest_path)) or {} except yaml.YAMLError as error: return [ Finding( severity="blocking", - code="superpowers-normalization-reference-invalid-yaml", - path=SUPERPOWERS_NORMALIZATION_PATH, - message=f"The obra/superpowers normalization reference is not valid YAML: {error}.", - suggestion="Fix the YAML syntax before relying on sync normalization.", + code="external-resource-manifest-invalid-yaml", + path=MANAGED_EXTERNAL_RESOURCES_PATH, + message=f"The consolidated manifest is not valid YAML: {error}.", + suggestion="Fix the YAML syntax so the manifest remains auditable.", ) ] - managed_skills = normalize_superpowers_managed_skills(payload) - if not managed_skills: + if not isinstance(payload, dict) or payload.get("version") != 1: return [ Finding( severity="blocking", - code="superpowers-normalization-reference-empty", - path=SUPERPOWERS_NORMALIZATION_PATH, - message="The obra/superpowers normalization reference does not declare managed skills.", - suggestion="Add the managed skill map from the retained migration plan.", + code="external-resource-manifest-invalid-shape", + path=MANAGED_EXTERNAL_RESOURCES_PATH, + message="The consolidated manifest must be a version 1 YAML mapping.", + suggestion="Set version: 1 and declare sources, normalizations, and watchlist.", ) ] - findings: list[Finding] = [] - for entry in managed_skills: - legacy_local = entry["legacy_local"] - local = entry["local"] - legacy_directory = root / ".github/skills" / legacy_local - if legacy_directory.exists(): - findings.append( - Finding( - severity="blocking", - code="superpowers-import-legacy-skill-directory", - path=legacy_directory.relative_to(root).as_posix(), - message="A managed obra/superpowers skill still uses the retired `obra-*` directory name.", - suggestion=f"Rename the skill directory to `.github/skills/{local}`.", - ) + raw_sources = payload.get("sources") + if not isinstance(raw_sources, dict) or not raw_sources: + return [ + Finding( + severity="blocking", + code="external-resource-manifest-invalid-shape", + path=MANAGED_EXTERNAL_RESOURCES_PATH, + message="The consolidated manifest must declare a non-empty sources mapping.", + suggestion="Add at least one source with pinned ref and assets.", ) + ] - for skill_file in (legacy_directory / "SKILL.md", root / ".github/skills" / local / "SKILL.md"): - if not skill_file.exists(): + findings: list[Finding] = [] + seen_local: set[str] = set() + for source_id, raw_source in raw_sources.items(): + if not isinstance(raw_source, dict): + continue + raw_assets = raw_source.get("assets") + if not isinstance(raw_assets, list): + continue + for raw_asset in raw_assets: + if not isinstance(raw_asset, dict): continue - skill_name = load_frontmatter(skill_file).get("name") - if skill_name == local: + local = raw_asset.get("local") + if not isinstance(local, str) or not local.strip(): continue - findings.append( - Finding( - severity="blocking", - code="superpowers-import-skill-name-mismatch", - path=skill_file.relative_to(root).as_posix(), - message="A managed obra/superpowers skill frontmatter name does not match its canonical local id.", - suggestion=f"Set `name: {local}` so the skill id matches the directory name.", + local = local.strip() + if local in seen_local: + findings.append( + Finding( + severity="blocking", + code="external-resource-manifest-duplicate-target", + path=local, + message=f"The managed target {local} is declared more than once.", + suggestion="Keep one manifest entry per managed local path.", + ) ) - ) + else: + seen_local.add(local) - findings.extend(check_superpowers_legacy_references(root, payload, managed_skills)) return findings -def normalize_superpowers_managed_skills(payload: dict[str, object]) -> list[dict[str, str]]: - raw_entries = payload.get("managed_skills") - if not isinstance(raw_entries, list): +def check_superpowers_import_naming(root: Path) -> list[Finding]: + manifest_path = root / MANAGED_EXTERNAL_RESOURCES_PATH + if not manifest_path.exists(): return [] - normalized_entries: list[dict[str, str]] = [] - for raw_entry in raw_entries: - if not isinstance(raw_entry, dict): + try: + payload = yaml.safe_load(read_text(manifest_path)) or {} + except yaml.YAMLError: + return [] + + if not isinstance(payload, dict): + return [] + + raw_sources = payload.get("sources") + if not isinstance(raw_sources, dict): + return [] + + superpowers_source = raw_sources.get("obra-superpowers") + if not isinstance(superpowers_source, dict): + return [] + + raw_assets = superpowers_source.get("assets") + if not isinstance(raw_assets, list): + return [] + + managed_skills: list[dict[str, str]] = [] + for raw_asset in raw_assets: + if not isinstance(raw_asset, dict): continue - upstream = raw_entry.get("upstream") - legacy_local = raw_entry.get("legacy_local") - local = raw_entry.get("local") - if not all(isinstance(value, str) and value.strip() for value in (upstream, legacy_local, local)): + local = raw_asset.get("local") + canonical_name = raw_asset.get("canonical_name") + if not ( + isinstance(local, str) + and local.strip() + and isinstance(canonical_name, str) + and canonical_name.strip() + ): continue - normalized_entries.append( + local_dir = local.strip().removeprefix(".github/skills/") + managed_skills.append( { - "upstream": upstream.strip(), - "legacy_local": legacy_local.strip(), "local": local.strip(), + "local_dir": local_dir, + "canonical_name": canonical_name.strip(), } ) - return normalized_entries + if not managed_skills: + return [] -def check_superpowers_legacy_references( - root: Path, - payload: dict[str, object], - managed_skills: list[dict[str, str]], -) -> list[Finding]: findings: list[Finding] = [] - legacy_tokens = {entry["legacy_local"] for entry in managed_skills} - upstream_reference_tokens = { - f"superpowers:{entry['upstream']}": entry["local"] for entry in managed_skills - } - - for relative_path in collect_superpowers_scan_paths(root, payload): - text = read_text(root / relative_path) - for legacy_token in sorted(legacy_tokens): - if legacy_token not in text: - continue - findings.append( - Finding( - severity="blocking", - code="superpowers-import-legacy-reference", - path=relative_path, - message=f"A live catalog asset still references retired local id `{legacy_token}`.", - suggestion="Replace managed obra/superpowers ids with the canonical `superpowers-*` ids.", - ) - ) - - for upstream_token, local in sorted(upstream_reference_tokens.items()): - if upstream_token not in text: - continue - findings.append( - Finding( - severity="blocking", - code="superpowers-import-upstream-reference", - path=relative_path, - message=f"A live catalog asset still references upstream skill id `{upstream_token}`.", - suggestion=f"Use the canonical local id `{local}` instead.", - ) + for entry in managed_skills: + skill_file = root / entry["local"] / "SKILL.md" + if not skill_file.exists(): + continue + skill_name = load_frontmatter(skill_file).get("name") + if skill_name == entry["canonical_name"]: + continue + findings.append( + Finding( + severity="blocking", + code="superpowers-import-skill-name-mismatch", + path=skill_file.relative_to(root).as_posix(), + message="A managed obra/superpowers skill frontmatter name does not match its canonical local id.", + suggestion=f"Set `name: {entry['canonical_name']}` so the skill id matches the directory name.", ) + ) return findings -def collect_superpowers_scan_paths(root: Path, payload: dict[str, object]) -> list[str]: - live_scan = payload.get("live_scan") if isinstance(payload.get("live_scan"), dict) else {} - raw_includes = live_scan.get("include") if isinstance(live_scan, dict) else None - includes = raw_includes if isinstance(raw_includes, list) else [] - ignored_files = set(IGNORED_SYNC_FILENAMES) - raw_ignored_files = live_scan.get("ignored_files") if isinstance(live_scan, dict) else None - if isinstance(raw_ignored_files, list): - ignored_files.update(item for item in raw_ignored_files if isinstance(item, str)) - - paths: set[str] = set() - for include in includes: - if not isinstance(include, str) or not include.strip(): - continue - candidate = root / include - if candidate.is_file() and should_scan_superpowers_path(root, candidate, ignored_files): - paths.add(candidate.relative_to(root).as_posix()) - continue - if not candidate.is_dir(): - continue - for child in candidate.rglob("*"): - if child.is_file() and should_scan_superpowers_path(root, child, ignored_files): - paths.add(child.relative_to(root).as_posix()) - - return sorted(paths) - - -def should_scan_superpowers_path(root: Path, path: Path, ignored_files: set[str]) -> bool: - relative_path = path.relative_to(root).as_posix() - if path.name in ignored_files: - return False - if any(part in IGNORED_SYNC_PARTS for part in path.relative_to(root).parts): - return False - if relative_path.startswith("tmp/"): - return False - return path.suffix in {".md", ".yaml", ".yml", ".json", ".jsonc", ".py", ".patch"} - - def check_broken_local_links(root: Path) -> list[Finding]: findings: list[Finding] = [] for path in collect_repository_owned_markdown_paths(root): diff --git a/.github/scripts/lib/shared.py b/.github/scripts/lib/shared.py index 772ef681..50415e2b 100644 --- a/.github/scripts/lib/shared.py +++ b/.github/scripts/lib/shared.py @@ -74,7 +74,7 @@ ) INVENTORY_PATH = ".github/INVENTORY.md" IMPORTED_ASSET_OVERRIDES_PATH = ".github/skills/local-agent-sync-external-resources/references/imported-asset-overrides.yaml" -SUPERPOWERS_NORMALIZATION_PATH = ".github/skills/local-agent-sync-external-resources/references/superpowers-normalization.yaml" +MANAGED_EXTERNAL_RESOURCES_PATH = ".github/skills/local-agent-sync-external-resources/references/managed-resources.yaml" @dataclass(frozen=True) diff --git a/tests/github/scripts/lib/test_external_resource_catalog_contract.py b/tests/github/scripts/lib/test_external_resource_catalog_contract.py new file mode 100644 index 00000000..9aac633e --- /dev/null +++ b/tests/github/scripts/lib/test_external_resource_catalog_contract.py @@ -0,0 +1,104 @@ +import sys +from pathlib import Path + +import pytest +import yaml + +REPO_ROOT = next( + parent for parent in Path(__file__).resolve().parents + if (parent / "AGENTS.md").exists() and (parent / ".github").exists() +) +sys.path.insert(0, str(REPO_ROOT / ".github/scripts")) + +from lib.catalog_checks import ( # noqa: E402 + check_external_resource_manifest, + check_superpowers_import_naming, +) + + +def _write_valid_managed_resources(root: Path) -> None: + manifest_dir = root / ".github/skills/local-agent-sync-external-resources/references" + manifest_dir.mkdir(parents=True, exist_ok=True) + manifest = manifest_dir / "managed-resources.yaml" + manifest.write_text( + """\ +version: 1 +sources: + obra-superpowers: + repository: https://github.com/obra/superpowers.git + ref: abc123 + assets: + - upstream: skills/brainstorming + local: .github/skills/superpowers-brainstorming + canonical_name: superpowers-brainstorming +watchlist: [] +""", + encoding="utf-8", + ) + + +def _write_superpowers_skill(root: Path, name: str) -> None: + skill_dir = root / ".github/skills/superpowers-brainstorming" + skill_dir.mkdir(parents=True, exist_ok=True) + (skill_dir / "SKILL.md").write_text( + f"---\nname: {name}\n---\nContent.\n", + encoding="utf-8", + ) + + +def test_catalog_check_uses_consolidated_manifest(tmp_path: Path) -> None: + _write_valid_managed_resources(tmp_path) + _write_superpowers_skill(tmp_path, "superpowers-brainstorming") + + findings = check_superpowers_import_naming(tmp_path) + + assert findings == [] + + +def test_catalog_check_rejects_duplicate_managed_target(tmp_path: Path) -> None: + manifest_dir = tmp_path / ".github/skills/local-agent-sync-external-resources/references" + manifest_dir.mkdir(parents=True, exist_ok=True) + manifest = manifest_dir / "managed-resources.yaml" + manifest.write_text( + """\ +version: 1 +sources: + source-a: + repository: https://example.com/a.git + ref: abc + assets: + - upstream: skills/one + local: .github/skills/same + canonical_name: same + source-b: + repository: https://example.com/b.git + ref: def + assets: + - upstream: skills/two + local: .github/skills/same + canonical_name: same-two +watchlist: [] +""", + encoding="utf-8", + ) + + findings = check_external_resource_manifest(tmp_path) + + assert any( + finding.code == "external-resource-manifest-duplicate-target" + for finding in findings + ) + + +def test_catalog_check_rejects_residual_managed_upstream_name( + tmp_path: Path, +) -> None: + _write_valid_managed_resources(tmp_path) + _write_superpowers_skill(tmp_path, "brainstorming") + + findings = check_superpowers_import_naming(tmp_path) + + assert any( + finding.code == "superpowers-import-skill-name-mismatch" + for finding in findings + ) From 90099931746f8119ed61d0cd992ddb8f6fb30957 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 13:20:45 +0200 Subject: [PATCH 35/59] refactor: simplify external resource sync contract --- .../local-sync-external-resources.agent.md | 110 ++---- .../SKILL.md | 267 +++----------- .../agents/openai.yaml | 6 +- .../references/catalog-decision-checklist.md | 47 --- .../references/external-watchlist.yaml | 64 ---- .../references/fingerprinting-contract.md | 102 ------ .../references/managed-resource-scope.md | 198 ----------- .../refresh-execution-guardrails.md | 39 --- .../references/superpowers-normalization.yaml | 64 ---- .../scripts/apply_imported_asset_overrides.py | 224 ------------ .../check_external_refresh_workspace.py | 110 ------ .../scripts/normalize_superpowers_imports.py | 330 ------------------ .../scripts/sync_resource_fingerprint.py | 93 ----- .../scripts/test_apply_overrides.py | 80 ----- .../scripts/test_normalize.py | 54 --- .../scripts/test_workspace_check.py | 51 --- 16 files changed, 75 insertions(+), 1764 deletions(-) delete mode 100644 .github/skills/local-agent-sync-external-resources/references/catalog-decision-checklist.md delete mode 100644 .github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml delete mode 100644 .github/skills/local-agent-sync-external-resources/references/fingerprinting-contract.md delete mode 100644 .github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md delete mode 100644 .github/skills/local-agent-sync-external-resources/references/refresh-execution-guardrails.md delete mode 100644 .github/skills/local-agent-sync-external-resources/references/superpowers-normalization.yaml delete mode 100644 .github/skills/local-agent-sync-external-resources/scripts/apply_imported_asset_overrides.py delete mode 100644 .github/skills/local-agent-sync-external-resources/scripts/check_external_refresh_workspace.py delete mode 100644 .github/skills/local-agent-sync-external-resources/scripts/normalize_superpowers_imports.py delete mode 100644 .github/skills/local-agent-sync-external-resources/scripts/sync_resource_fingerprint.py delete mode 100644 tests/github/skills/local-agent-sync-external-resources/scripts/test_apply_overrides.py delete mode 100644 tests/github/skills/local-agent-sync-external-resources/scripts/test_normalize.py delete mode 100644 tests/github/skills/local-agent-sync-external-resources/scripts/test_workspace_check.py diff --git a/.github/agents/local-sync-external-resources.agent.md b/.github/agents/local-sync-external-resources.agent.md index f77d43ea..c6b80780 100644 --- a/.github/agents/local-sync-external-resources.agent.md +++ b/.github/agents/local-sync-external-resources.agent.md @@ -1,6 +1,6 @@ --- name: local-sync-external-resources -description: Use this agent when applying, auditing, or planning changes to the declared sync-managed GitHub Copilot catalog in this repository, including keep/update/extract/retire decisions and governance-drift cleanup within the approved managed scope. +description: Use this agent when applying, auditing, or planning declared external resource refreshes through the staged sync CLI. tools: ["read", "edit", "search", "execute", "web"] disable-model-invocation: true agents: [] @@ -10,106 +10,36 @@ agents: [] ## Role -You are the source-side sync and catalog-governance wrapper for this -repository's GitHub Copilot customization assets. - -Use this agent for route selection, managed-scope boundary decisions, approval -posture, and completion expectations. Keep the reusable keep, update, extract, -retire, and anti-drift procedure in the paired core skill. +You are the manifest-driven sync wrapper for this repository's declared +external resource refreshes. Use this agent for audit, plan, and apply +operations through the single canonical CLI. ## Core Skill - `local-agent-sync-external-resources` -## Routing Rules - -- Use this agent for source-side `.github/` catalog governance inside the - declared managed external scope. -- Use this agent when the task is about catalog coherence, naming - normalization, overlap removal, governance drift, or repo-owned replacements - across the managed catalog. -- Treat `sync` as `apply` by default unless the user explicitly asks for an - audit, plan, or dry run. -- Treat `apply` as invalid until `internal-copilot-audit` has completed - preflight and no unresolved `blocking` findings remain. -- Start with `internal-gateway-idea` when the catalog problem still - needs option framing, staged planning, or a user-supplied multi-step - remediation plan. -- Use `internal-agent-creator` when the task is one concrete agent contract, - agent routing boundary, or agent/skill split rather than catalog-wide sync - governance. -- Do not use this agent for target-repository baseline propagation; recommend - `local-sync-global-copilot-configs-into-repo` instead. -- When current platform behavior decides policy, validate it through - `internal-copilot-docs-research` before changing the sync contract. - -## Boundary Definition +## Boundary -- Stay in this lane while the work is source-side `.github/` catalog governance - inside the declared managed scope. -- Keep prefix ownership, imported-resource posture, and source-side propagation - boundaries visible in this wrapper. -- If the request is really source-side planning, consumer-repository sync, or a - local edit outside catalog-governance scope, explain the mismatch and - recommend the better owner visibly. -- Do not route, dispatch, or delegate from this lane. +- `sync` means `apply` by default unless the user explicitly asks for `audit` or `plan`. +- Refuse `apply` when a managed target has uncommitted changes unless the user supplies `--allow-dirty`. +- Stage all fetched and transformed resources outside the repository. +- Do not modify repository targets until the complete candidate tree, normalizations, overrides, and generated patch pass validation. +- Do not refresh or modify any imported skill while implementing sync tooling. -## Scope Contract +## Safety -- This agent owns the visible source-side catalog boundary and the approval - posture for sync-managed assets. -- Repository-owned source assets use `internal-*` by default; source-only sync - tooling uses `local-*`; imported assets keep their managed local ids unless an - approved replacement takes over. -- Imported assets are support depth by default. Prefer an `internal-*` owner - only when routing, governance, terminology, output shape, safety - expectations, or a missing owner requires it. -- Every approved imported in-place override must be mapped in - `.github/skills/local-agent-sync-external-resources/references/imported-asset-overrides.yaml` - and replayable through the bundled override script. -- Allow a direct in-place override only for a strong repo-specific need that - the user explicitly counter-validates and registers in the approved override - bundle. -- Use - `.github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md` - for the exact upstream family map, retained support-only office posture, and - approved imported-override context. -- Active `mattpocock/skills` imports in scope are `code-review` -> - `mattpocock-code-review`, `grill-me` -> `grill-me`, - `handoff` -> `mattpocock-handoff` (productivity), and `research` -> - `mattpocock-research` (engineering); keep retired Matt Pocock imports - (including the previously retained `caveman` -> `mattpocock-caveman`) - out of the live managed scope and tracked only in the alert-only watchlist. -- Active `vercel-labs/skills` imports in scope are `find-skills` -> - `vercel-find-skills`; the `vercel` prefix is narrow to this family and must - not be reused for other imported families. -- Do not add new prefixes, external families, compatibility aliases, or hidden - imported forks unless the user explicitly expands scope. -- When catalog meaning changes, re-check root `AGENTS.md`, - `.github/copilot-instructions.md`, and `.github/INVENTORY.md` in the same - pass. -- Keep retained sync evidence under repository-root `tmp/`. +- Workspace must be outside the repository. +- Dirty managed targets block `apply` unless `--allow-dirty` is supplied. +- Override replay is atomic: if any override fails, no candidate changes reach the repository. -## Output Expectations - -Follow the completion-report contract from `.github/copilot-instructions.md`. +## Completion Output In `Outcome`, include: - `Mode`: `apply`, `audit`, or `plan`. -- `Catalog scope`: files reviewed and why. -- `Governance files reviewed`: whether `.github/copilot-instructions.md` and - root `AGENTS.md` were reviewed, changed, or intentionally left unchanged. -- `Canonical decisions`: `keep`, `update`, `extract`, or `retire`. +- `Workspace`: external staging path or `n/a`. +- `Managed assets`: count from the manifest. +- `Changed paths`: list of repository-relative paths that changed. +- `Override results`: status of each replayed override. - `Validation`: commands run and remaining gaps. -- `Remaining blockers or drift`: unresolved issues that prevent or narrow - `apply`. - -Add the following outcome details when refresh execution is involved: - -- `Workspace guard`: where upstream snapshots were staged and whether the - bundled workspace guard passed. -- `Graphify guard`: whether graphify ran after repo-local refresh leftovers - were absent. -- `Scoped validation`: whether whitespace and diff checks were scoped away from - verbatim upstream content, with any accepted upstream notices named. +- `Blockers`: unresolved issues that prevent or narrow `apply`. diff --git a/.github/skills/local-agent-sync-external-resources/SKILL.md b/.github/skills/local-agent-sync-external-resources/SKILL.md index 16850988..62a57143 100644 --- a/.github/skills/local-agent-sync-external-resources/SKILL.md +++ b/.github/skills/local-agent-sync-external-resources/SKILL.md @@ -1,239 +1,76 @@ --- name: local-agent-sync-external-resources -description: Use when maintaining the sync-managed `.github/` catalog behind `local-sync-external-resources`, especially for keep/update/extract/retire decisions, approved external refreshes, and governance-drift checks. +description: Audit, plan, or apply declared external resource refreshes through the staged sync CLI. --- -# Internal Agent Sync External Resources +# Local Agent Sync External Resources -## Referenced skills +## Contract -- `internal-skill-creator`: route repository-owned skill creation, replacement, or material rewriting. -- `openai-skill-creator`: use only for remaining bundle anatomy after local skill boundaries are settled. -- `internal-agent-creator`: route external-resources agent changes or agent/skill boundary rewrites. +This skill owns the manifest-driven CLI that stages, validates, and applies +declared external resource refreshes safely. The single public entrypoint is +`scripts/sync_external_resources.py`. -Use this skill as the default operating engine for `.github/agents/local-sync-external-resources.agent.md`. +## Modes -This skill owns the reusable catalog-governance procedure behind that agent. Keep the agent focused on routing, managed scope, approval posture, and output contract. Keep the reusable sync workflow, decision rules, and anti-drift checks here. +- `audit`: parse all registries, validate local paths, canonical names, hashes, + watchlist shape, and dirty state. Does not fetch or write. +- `plan`: require an external workspace, build and validate the complete + candidate, then emit the changed-path summary without repository writes. +- `apply`: perform `plan`, reject dirty targets unless `--allow-dirty`, + generate one patch, run `git apply --check`, apply once, rebuild inventory, + and rerun scoped validation. -Use the current repository state as evidence and starting context, but anchor decisions to the declared contract in the relevant agent, root `AGENTS.md`, and `.github/copilot-instructions.md`. +## Safety -When exact upstream family scope, normalized local ids, retained support-only -posture, or approved imported-override context matters, read -`references/managed-resource-scope.md`. - -Use `internal-skill-creator` first when a sync decision requires creating, replacing, or materially rewriting one repository-owned skill. It is the canonical local entrypoint for repository-owned skill work in this repository. - -Use `openai-skill-creator` only for the remaining bundle anatomy, helper scripts, progressive disclosure, `agents/openai.yaml`, or structural validation work after `internal-skill-creator` has established the repository-owned skill boundary and decided the local ownership outcome. - -Use `internal-agent-creator` when the sync changes this external-resources agent, rewrites agent routing boundaries, or changes how the agent/skill split is structured. - -When deterministic change detection matters, read `references/fingerprinting-contract.md`, use the canonical repository implementation in `.github/scripts/lib/fingerprinting.py`, and use `scripts/sync_resource_fingerprint.py` from this skill only as a thin workflow wrapper. - -When an imported upstream asset has an exceptionally approved repo-local override, read `references/imported-asset-overrides.yaml` and use `scripts/apply_imported_asset_overrides.py` to replay the registered patch bundle after refreshing the upstream content. - -When upstream refresh execution may fetch, clone, unpack, or compare external -repositories, read `references/refresh-execution-guardrails.md` first and run -`scripts/check_external_refresh_workspace.py` before graphify or completion -reporting. - -When refreshing the managed `obra/superpowers` family, read `references/superpowers-normalization.yaml` and use `scripts/normalize_superpowers_imports.py` so upstream skill names materialize as local `superpowers-*` ids before override replay, inventory rebuild, and validation. - -When checking upstream items that were deliberately extracted or retired from -the managed catalog, read `references/external-watchlist.yaml`. The watchlist is -alert-only evidence; it does not reimport assets, decide keep/retire outcomes, -or imply automation unless a script is explicitly added later. - -## When to use - -- Maintain the sync-managed `.github/` catalog behind `local-sync-external-resources`. -- Make keep, update, extract, retire, or approved external-refresh decisions across the managed catalog. -- Keep approved imported-asset override exceptions explicit, replayable, and auditable instead of leaving hidden forks. -- Resolve governance drift, overlap, or managed-scope ambiguity in the source repository. - -## Goals - -- Keep one clear canonical asset per intent across the managed `.github/` catalog. -- Make `local-sync-external-resources` visibly depend on one named operating engine instead of an implicit catalog skill. -- Refresh only approved in-scope external-prefixed assets without expanding scope accidentally. -- Keep approved imported in-place exceptions narrow, mapped, and re-applicable after future refreshes. -- Move reusable sync procedure into this skill instead of bloating the agent body. -- Keep naming, frontmatter, links, descriptions, and governance references deterministic. -- Keep the managed `obra/superpowers` family normalized to local `superpowers-*` ids; treat `obra-*` and `superpowers:` in live catalog assets as blocking drift. -- Remove fallback, alias, deprecated, or compatibility-only drift in the same pass that introduces the canonical replacement. -- Keep low-frequency imported or internal capabilities documented as on-demand depth when they still add distinct value and do not justify a repository-owned wrapper. -- Keep governance review of root `AGENTS.md` and `.github/copilot-instructions.md` explicit, never implied. -- Provide a deterministic fingerprinting workflow that distinguishes real content change from formatting noise. - -## Agent Coupling Contract - -For `local-sync-external-resources`, keep the split strict: - -- Agent owns routing, scope boundaries, managed resource map, approval posture, and output expectations. -- This skill owns audit order, keep/update/extract/retire decisions, anti-overlap heuristics, and sync execution discipline. -- `internal-agent-creator` owns structural changes to the agent itself, including mandatory engine-skill architecture and boundary rewrites. -- `internal-skill-creator` owns repository-owned skill authoring and should be the first local route when one concrete skill needs creation, replacement, or major redesign. -- `openai-skill-creator` covers only the remaining bundle mechanics during that work; it should not replace the local decision gate or duplicate repository-owned routing logic. -- `references/imported-asset-overrides.yaml` owns the approved imported in-place override registry for this repository. -- `scripts/apply_imported_asset_overrides.py` owns patch replay after an upstream refresh; prefer a clean replay first, allow a registered `git apply --3way` fallback when upstream text drift is compatible, and stop for review instead of forcing a hidden fork. -- `references/superpowers-normalization.yaml` owns the upstream-to-local map, blocked legacy ids, and live scan scope for the `obra/superpowers` family. -- `scripts/normalize_superpowers_imports.py` owns local id normalization after upstream materialization and before imported override replay. -- Repo-local path rewrites that must survive every `obra/superpowers` refresh belong in `references/superpowers-normalization.yaml` and are applied by `scripts/normalize_superpowers_imports.py`, not by ad hoc manual edits after import. - -Do not collapse these roles back into one file just because the current task touches all of them. - -## Decision Order - -1. Check the declared managed scope in `local-sync-external-resources` plus the live local inventory and nearby trigger space. -2. Decide whether the capability should remain an `internal-*` asset, remain an approved in-scope external-prefixed asset, or be retired. -3. For imported assets, prefer verbatim refresh first. Allow a direct in-place override only when the reason is strong, the user explicitly counter-validates it, and the replay patch is registered in this skill bundle. -4. Prefer consolidation over coexistence when two assets compete for the same trigger space. -5. Repair broken references only when the asset still adds distinct value to the declared catalog. -6. Apply the canonical change first, then remove deprecated duplicates, stale references, and hollow dependencies in the same pass. -7. Re-check root `AGENTS.md` and `.github/copilot-instructions.md` whenever catalog meaning, routing, or governance language changed. - -## Classification Matrix - -| Case | Action | -| --- | --- | -| Repo-specific sync workflow or governance logic | Create or update an `internal-*` asset | -| External-resources reusable operating logic | Keep it in this skill | -| Installed external-prefixed asset still useful and declared in scope | Refresh in place | -| Imported asset needs a rare repo-local exception after refresh | Require explicit user counter-validation, register the patch in `references/imported-asset-overrides.yaml`, and replay it with `scripts/apply_imported_asset_overrides.py` | -| Thin alias, fallback copy, or deprecated variant | Delete the weaker asset | -| Broken or stale asset with no unique value | Retire it | -| Installed capability is still distinct but low-frequency or on-demand | Keep it as dormant support depth and document why; do not wrap it unless the root wrapper threshold is met | -| Agent body becoming procedural or duplicative | Extract procedure into this skill or the right internal skill | -| Agent routing or engine-skill split changing materially | Use `internal-agent-creator` | - -Load `references/catalog-decision-checklist.md` when you need the detailed keep/update/extract/retire heuristics or overlap tests. +- Workspace must be outside the repository. +- Dirty managed targets block `apply` unless `--allow-dirty` is supplied. +- Override replay is atomic: if any override fails verification, no candidate + changes reach the repository. +- Do not modify repository targets until the complete candidate tree, + normalizations, overrides, and generated patch pass validation. ## Workflow -### 1. Inventory Before Editing - -- Read the target asset and the closest competing assets. -- Compare `description:` lines first. Trigger overlap starts there. -- Check whether the repository already has a stronger internal equivalent. -- Check whether the asset references files that do not exist. -- Check whether nearby agents, skills, instructions, `AGENTS.md`, or `.github/copilot-instructions.md` still route to the asset. -- Check whether the asset still belongs to the declared managed scope or only survives due to repository drift. - -### 2. Pick the Right Outcome - -Use these heuristics: - -- Keep both only when they serve clearly different intents. -- Merge only when the surviving asset becomes easier to trigger and easier to maintain. -- Delete when one asset is just a noisier, thinner, or less structured version of another. -- Keep dormant imported or internal capabilities when they still provide distinct on-demand value and the root wrapper threshold is not met. -- Create or update an internal asset when the capability is strategic for this repository and should not depend on external wording or lifecycle. -- Refresh an in-scope external-prefixed asset only when it still adds distinct value to the declared managed catalog. -- For imported assets with approved repo-local exceptions, refresh the upstream content first and replay only the registered patch bundle afterward. +1. Run `audit` to confirm the manifest and overrides parse cleanly. +2. Run `plan` with an external workspace to build the candidate without + repository writes. +3. Review the changed-path summary and override results. +4. Run `apply` to stage, validate, and commit the patch in one operation. -### 3. Author or Refresh Carefully +## Canonical Commands -Required frontmatter for skills: - -```yaml ---- -name: internal-example -description: Clear trigger language that says when the skill should be used. ---- +```bash +python3 .github/skills/local-agent-sync-external-resources/scripts/sync_external_resources.py audit +python3 .github/skills/local-agent-sync-external-resources/scripts/sync_external_resources.py plan --workspace /private/tmp/cloud-strategy-github-external-refresh +python3 .github/skills/local-agent-sync-external-resources/scripts/sync_external_resources.py apply --workspace /private/tmp/cloud-strategy-github-external-refresh ``` -Rules: - -- `name:` must match the directory name exactly. -- Put trigger language in `description:`, not buried in the body. -- Keep `description:` focused on triggering conditions; do not summarize the workflow there. -- Keep repository-facing text in English. -- Keep the local canonical identifier when refreshing an installed external-prefixed asset. -- Do not keep runtime-specific clutter, compatibility prose, or history-preserving aliases unless policy explicitly requires them. - -### 4. Normalize Managed Superpowers Imports +## Override Rules -For the `obra/superpowers` family: +- Every approved imported in-place override must be registered in + `references/imported-asset-overrides.yaml` with a replay patch and expected + content hash. +- Replay uses clean `git apply --check` first, then `--3way --check` only when + declared. +- Stop for review if neither path applies cleanly. -1. Read `references/superpowers-normalization.yaml` before materializing files. -2. Install or refresh only the managed upstream skills listed in that reference. -3. Materialize local directories and frontmatter as `superpowers-*`, not `obra-*`. -4. Keep repo-local workspace paths in that same reference. For retained-plan and retained-spec paths, rewrite the upstream `docs/` Superpowers workspace references to `tmp/superpowers` there so every refresh reapplies the local storage contract automatically. -5. Run `scripts/normalize_superpowers_imports.py --check` before the rename to expose drift. -6. Run `scripts/normalize_superpowers_imports.py --apply` after materialization or rename. -7. Run `scripts/apply_imported_asset_overrides.py --dry-run` after normalization and before applying overrides. -8. Rebuild `.github/INVENTORY.md` and run catalog validation. - -Treat residual managed `obra-*` ids or `superpowers:` references in live catalog assets as blocking drift. Do not add a newly discovered upstream Superpowers skill unless the user explicitly expands the managed map. -Treat residual upstream Superpowers `docs/` workspace references in the refreshed managed assets as blocking drift when the repo-local contract still requires `tmp/superpowers`. - -### 5. Keep Sync Evidence Deterministic - -When a sync workflow needs evidence that a managed resource truly changed: - -- Prefer deterministic comparison over visual or memory-based judgment. -- Read `references/fingerprinting-contract.md` before changing the comparison contract. -- Use `scripts/sync_resource_fingerprint.py snapshot ...` to build a retained manifest when the workflow benefits from repeatable evidence. -- Use `scripts/sync_resource_fingerprint.py diff ...` to compare two manifests instead of hand-reviewing large file sets. -- Compare normalized content, not only the raw fetched file. -- Record source, target path, normalization version, and content hash when a retained manifest materially improves safety. -- Keep any retained sync evidence under repository-root `tmp/` and treat it as auxiliary workflow output, not catalog policy. -- Do not introduce hashing manifests or helper scripts as decorative machinery; add them only when they clearly reduce false positives, repeated work, or unsafe refresh decisions. -- Use the normalization rules, manifest schema, and output defaults from `references/fingerprinting-contract.md` instead of forking them inline. - -### 6. Replay Approved Imported Overrides - -When an imported upstream asset has an approved repo-local exception: - -1. Refresh the imported asset verbatim from the declared upstream source. -2. Check that the target path is listed in `references/imported-asset-overrides.yaml`. -3. Run `scripts/apply_imported_asset_overrides.py --id ` or the full bundle when replaying every approved override after the refresh. -4. If clean replay fails, allow only the registered `git apply --3way` fallback. If that also fails, or if the post-apply content hash does not match the registry, stop and review the exception instead of forcing the patch through. -5. Reassess whether an `internal-*` wrapper or replacement now serves the repository better than continuing the imported override. +## Validation -### 7. Re-check Governance Immediately +- `python3 -m compileall .github/skills/local-agent-sync-external-resources/scripts` +- `python3 -m pytest -q tests/github/skills/local-agent-sync-external-resources/scripts` +- `python3 .github/scripts/validate_internal_skills.py --skill local-agent-sync-external-resources --strict` +- `make inventory-build` +- `make token-risks` -After catalog changes: +## Output -- Re-check root `AGENTS.md` for routing, naming, bridge, and canonical-owner drift. -- Re-check `.github/copilot-instructions.md` for repo-wide projection drift. -- Re-check `.github/INVENTORY.md` for exact path accuracy. -- Re-check nearby agents and skills for stale references, decorative declarations, or broken ownership assumptions. -- Re-check `LESSONS_LEARNED.md` and ask whether any pending lesson was just codified or disproven so the ledger does not retain a duplicate. -- Use the external watchlist only to notice upstream changes that might deserve - a future human review; do not treat watchlist entries as managed scope. +Report: mode, workspace, managed count, changed paths, override results, +validation, and blockers. -## Validation +## Anti-Scope -Before finishing: - -- Confirm `name:` equals the folder name. -- Confirm every referenced local file exists. -- Confirm representative bundled scripts run successfully when added or changed. -- Confirm the description is specific enough to trigger, but not broad enough to collide with half the catalog. -- Confirm the skill remains in English. -- Confirm inventory and governance files do not point to removed paths. -- Confirm the agent still names this skill explicitly as its engine or default operating workflow. -- Confirm the change did not leave decorative skill declarations behind. -- Confirm no fallback alias or compatibility-only duplicate remains beside the canonical asset. -- Confirm every approved imported in-place override is registered in `references/imported-asset-overrides.yaml`, has a live patch file, and still matches the expected normalized content hash. - -## Anti-Patterns - -- Keeping duplicate assets "just in case." -- Refreshing an external asset just because upstream changed when the local catalog does not need it. -- Leaving a repo-local imported override undocumented, unapproved, or without a replay patch. -- Forcing a replay patch after `git apply --check` failed. -- Creating machinery-heavy sync helpers that never become part of a real workflow. -- Creating a control-center engine skill that merely says "see another skill." -- Leaving retired or deprecated assets in the live catalog. -- Hiding important trigger words deep in the body instead of the description. - -## Handoff - -When this skill is used from `local-sync-external-resources`: - -1. Audit the catalog. -2. Decide keep, refresh, replace, extract, or retire. -3. Apply the catalog changes. -4. Re-check root `AGENTS.md`, `.github/copilot-instructions.md`, and `.github/INVENTORY.md`. -5. Run repository validation. +- Do not refresh or modify any imported skill while implementing sync tooling. +- Do not add a plugin system, concurrency, compatibility aliases, legacy + fallback paths, or a generic sync framework. +- Do not perform a live network refresh unless the user separately authorizes it. diff --git a/.github/skills/local-agent-sync-external-resources/agents/openai.yaml b/.github/skills/local-agent-sync-external-resources/agents/openai.yaml index 0b954289..7f22080e 100644 --- a/.github/skills/local-agent-sync-external-resources/agents/openai.yaml +++ b/.github/skills/local-agent-sync-external-resources/agents/openai.yaml @@ -1,4 +1,4 @@ interface: - display_name: "Internal Agent Sync External Resources" - short_description: "Govern sync catalog decisions safely." - default_prompt: "Use $local-agent-sync-external-resources to audit the managed .github catalog, decide keep/update/extract/retire outcomes, and use the bundled fingerprinting script when deterministic change evidence is needed." + display_name: "Local Sync External Resources" + short_description: "Audit, plan, or apply declared external refreshes." + default_prompt: "Use $local-agent-sync-external-resources to refresh only declared managed external resources through the staged sync CLI." diff --git a/.github/skills/local-agent-sync-external-resources/references/catalog-decision-checklist.md b/.github/skills/local-agent-sync-external-resources/references/catalog-decision-checklist.md deleted file mode 100644 index 084058fc..00000000 --- a/.github/skills/local-agent-sync-external-resources/references/catalog-decision-checklist.md +++ /dev/null @@ -1,47 +0,0 @@ -# Catalog Decision Checklist - -Use this reference when a sync review needs detailed keep/update/extract/retire heuristics beyond the main workflow. - -## Overlap Review Checklist - -Delete or replace an asset when most of these are true: - -- the description triggers on the same requests as another installed asset -- the competing asset is more structured or more complete -- the weaker asset adds no distinctive workflow -- the weaker asset routes to missing resources or stale instructions -- the repository already has an internal asset that should own the domain - -Keep specialized subskills only when they narrow trigger space instead of broadening collision. - -## Refresh Rules - -When refreshing an installed external-prefixed asset: - -1. Keep the existing local identifier and prefix. -2. Preserve only the capability that still maps to the current repository. -3. Remove stale runtime assumptions, deprecated frontmatter, and broken bundled references. -4. Do not add new sibling assets from the same family unless the user explicitly expands scope. -5. Update governance files only when routing or inventory meaningfully changes. - -## Approved Imported Override Rules - -Use a direct imported in-place override only when all of these are true: - -- the repo-specific need is strong enough that a wrapper or replacement is not the better immediate fix -- the user explicitly counter-validates the exception -- the target is registered in `references/imported-asset-overrides.yaml` -- the replay patch lives under `patches/` -- the override can be replayed after a verbatim upstream refresh without manual guesswork - -Refresh upstream content first, then replay the registered patch. Prefer a clean replay first; if the registry allows `git apply --3way`, use that fallback only when upstream text drift is still compatible. If neither path applies cleanly, stop and review the exception instead of forcing it. - -## Extraction Rules - -When `local-sync-external-resources` or a nearby sync asset is turning into a knowledge dump: - -1. Keep the agent cohesive around routing, managed scope, approval posture, and orchestration. -2. Move long reusable procedures into this skill or the right existing internal skill. -3. Use `internal-agent-creator` if the extraction changes the agent's structural contract. -4. Point the agent at the canonical skill explicitly. -5. Keep the extracted workflow reusable outside the single current task. diff --git a/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml b/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml deleted file mode 100644 index 8bf7068e..00000000 --- a/.github/skills/local-agent-sync-external-resources/references/external-watchlist.yaml +++ /dev/null @@ -1,64 +0,0 @@ -version: 1 -items: - - source_family: github/awesome-copilot - upstream_id: azure-devops-pipelines.instructions.md - local_owner: internal-azure-devops - reason: Azure DevOps pipeline guidance was extracted into a repository-owned skill-first owner. - action: alert-only - - source_family: github/awesome-copilot - upstream_id: go.instructions.md - local_owner: internal-go - reason: Go guidance was extracted into a repository-owned skill-first owner. - action: alert-only - - source_family: github/awesome-copilot - upstream_id: kubernetes-manifests.instructions.md - local_owner: internal-kubernetes - reason: Kubernetes manifest guidance was extracted into repository-owned Kubernetes skills. - action: alert-only - - source_family: github/awesome-copilot - upstream_id: shell.instructions.md - local_owner: internal-bash - reason: Shell guidance was extracted into the repository-owned Bash baseline and script skill split. - action: alert-only - - source_family: mattpocock/skills - upstream_id: diagnose - local_owner: internal-debugging - reason: Root-cause diagnosis loop was extracted into a repository-owned owner. - action: alert-only - - source_family: mattpocock/skills - upstream_id: tdd - local_owner: internal-tdd - reason: Test-first delivery rules were extracted into a repository-owned owner. - action: alert-only - - source_family: mattpocock/skills - upstream_id: improve-codebase-architecture - local_owner: internal-review-high-level - reason: Architecture deepening lenses were extracted into high-level review. - action: alert-only - - source_family: mattpocock/skills - upstream_id: zoom-out - local_owner: internal-review-high-level - reason: Codebase orientation, module maps, caller maps, and domain vocabulary were extracted into high-level review. - action: alert-only - - source_family: mattpocock/skills - upstream_id: grill-with-docs - local_owner: internal-gateway-writing-plans - reason: Glossary and ADR side effects are not default repository conventions. - action: alert-only - - source_family: mattpocock/skills - upstream_id: setup-matt-pocock-skills - local_owner: local-agent-sync-external-resources - reason: Setup conventions are not part of the managed source catalog. - action: alert-only - - source_family: mattpocock/skills - upstream_id: caveman - local_owner: retired - reason: The previously retained `caveman` -> `mattpocock-caveman` import - was retired from the managed catalog by explicit user scope reduction. - Do not reimport unless the user re-approves the retained import. - action: alert-only - - source_family: addyosmani/agent-skills - upstream_id: idea-refine - local_owner: internal-gateway-idea - reason: Useful shaping frameworks and evaluation criteria were extracted into the repository-owned idea gateway; the competing autonomous workflow was retired. - action: alert-only diff --git a/.github/skills/local-agent-sync-external-resources/references/fingerprinting-contract.md b/.github/skills/local-agent-sync-external-resources/references/fingerprinting-contract.md deleted file mode 100644 index 0742bfb2..00000000 --- a/.github/skills/local-agent-sync-external-resources/references/fingerprinting-contract.md +++ /dev/null @@ -1,102 +0,0 @@ -# Fingerprinting Contract - -Use this reference when a sync or audit workflow needs deterministic proof that a managed resource changed. - -## Purpose - -The goal is not to hash files for its own sake. The goal is to reduce false positives and avoid unsafe refresh decisions. - -Use fingerprinting when one or more of these are true: - -- a managed asset family is large enough that visual diff review is slow -- upstream refreshes are frequent and noisy -- the workflow needs a retained manifest to compare two runs -- the same comparison logic would otherwise be rewritten ad hoc - -Do not add or keep manifests when they provide no real safety benefit. - -## Manifest Schema - -Each resource entry should follow this shape: - -```json -{ - "resource_id": "openai/skills/doc", - "target_path": ".github/skills/openai-docx/SKILL.md", - "source_ref": "https://github.com/openai/skills/tree/45d05d75363abf13f99d09e899d61e07b8010685/skills/.curated/doc/SKILL.md", - "kind": "skill", - "normalization_version": "v1", - "hash_algo": "sha256", - "source_hash": "raw-bytes-hash", - "content_hash": "normalized-content-hash", - "metadata": { - "mapped_name": "openai-docx" - } -} -``` - -Use commit-hash `source_ref` values in manifests. When a Markdown catalog documents the same upstream source, add an inline note with the release tag when one exists, or with the commit date when no tagged release exists. - -Manifest-level fields should include: - -- `generated_at` -- `root` -- `normalization_version` -- `hash_algo` -- `resources` - -## Output Paths - -Use these defaults consistently: - -- skill-scoped temporary evidence: `tmp/superpowers/internal-agent-sync-control-center.manifest.json` -- other ad hoc fingerprint outputs for this workflow: under `tmp/superpowers/` -- canonical repository sync manifest written by the repo sync flow: `.github/copilot-sync.manifest.json` - -Do not treat files under `tmp/` as catalog policy or durable governance state. - -## Normalization Rules - -For `v1`, normalize text conservatively: - -1. convert `CRLF` and `CR` to `LF` -2. strip trailing whitespace from each line -3. collapse repeated trailing blank lines -4. ensure exactly one final newline - -Do not reorder sections, markdown bullets, or frontmatter keys in `v1`. Treat order as meaningful until the repository intentionally adopts a stronger canonicalization rule. - -## Kind Detection - -Infer `kind` from the path when possible: - -- `.github/skills/*/SKILL.md` -> `skill` -- `.github/agents/*.agent.md` -> `agent` -- fallback -> `file` - -## Real Change vs Noise - -Interpret manifest diffs this way: - -- different `source_hash` and different `content_hash`: real content change -- different `source_hash` and same `content_hash`: normalization-only noise -- same `source_hash` and same `content_hash`: unchanged - -Treat `normalization-only` as a signal to review the pipeline, not as a default reason to refresh the managed asset. - -## Suggested Workflow - -1. Generate a baseline snapshot into `tmp/`. -2. Generate a candidate snapshot after refresh or fetch. -3. Diff the two manifests. -4. Use the diff report to decide whether `apply` is justified. - -## Bundled Script - -Use `scripts/sync_resource_fingerprint.py` for: - -- snapshot generation -- normalized hashing -- manifest diffing - -Keep the skill contract here and the deterministic execution logic in the script. diff --git a/.github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md b/.github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md deleted file mode 100644 index 792e85d6..00000000 --- a/.github/skills/local-agent-sync-external-resources/references/managed-resource-scope.md +++ /dev/null @@ -1,198 +0,0 @@ -# Managed Resource Scope - -Use this reference when `local-sync-external-resources` needs the exact managed -upstream family map, normalized local ids, retained support-only posture, or -approved imported-override context without re-expanding that detail in the -wrapper agent. - -## Ownership Posture - -- `internal-*` is the default prefix for repository-owned resources in this - standards repository. -- `local-*` is reserved for source-only sync tooling in this repository. -- Imported upstream resources keep their external-prefixed local ids unless an - approved repository-owned replacement or managed normalization takes over. -- Imported assets are support depth by default. Prefer an `internal-*` owner - only when routing, governance, terminology, output shape, safety - expectations, or a missing owner requires it. -- Approved imported in-place overrides must stay registered in - `references/imported-asset-overrides.yaml` and replayable through the paired - scripts. - -## Managed Families - -### `github/awesome-copilot` - -Sources: - -- Agents: - `https://github.com/github/awesome-copilot/tree/e986f49695491311df2774030ebe11efabd0fb77/agents` - (commit date: 2026-07-03) -- Skills: - `https://github.com/github/awesome-copilot/tree/e986f49695491311df2774030ebe11efabd0fb77/skills` - (commit date: 2026-07-03) -- Retired instruction source: - `https://github.com/github/awesome-copilot/tree/e986f49695491311df2774030ebe11efabd0fb77/instructions` - (commit date: 2026-07-03; alert-only watchlist, not managed source scope) - -Managed assets: - -- Agents: none. -- Skills: `agentic-eval` -> `awesome-copilot-agentic-eval`; - `azure-devops-cli` -> `awesome-copilot-azure-devops-cli`; `azure-pricing` -> - `awesome-copilot-azure-pricing`; `azure-resource-health-diagnose` -> - `awesome-copilot-azure-resource-health-diagnose`; `azure-role-selector` -> - `awesome-copilot-azure-role-selector`; `cloud-design-patterns` -> - `awesome-copilot-cloud-design-patterns`; `codeql` -> - `awesome-copilot-codeql`; `dependabot` -> `awesome-copilot-dependabot`; - `secret-scanning` -> `awesome-copilot-secret-scanning`. -Retired upstream instruction assets are tracked in the alert-only watchlist when -their content has a repository-owned replacement owner. - -### `obra/superpowers` - -Source: - -- Skills: - `https://github.com/obra/superpowers/tree/d884ae04edebef577e82ff7c4e143debd0bbec99/skills` - (release tag: v6.1.1; commit date: 2026-07-02) - -Managed skills: - -- `brainstorming` -> `superpowers-brainstorming`; - `dispatching-parallel-agents` -> `superpowers-dispatching-parallel-agents`; - `executing-plans` -> `superpowers-executing-plans`; - `finishing-a-development-branch` -> - `superpowers-finishing-a-development-branch`; - `receiving-code-review` -> `superpowers-receiving-code-review`; - `requesting-code-review` -> `superpowers-requesting-code-review`; - `subagent-driven-development` -> - `superpowers-subagent-driven-development`; - `systematic-debugging` -> `superpowers-systematic-debugging`; - `test-driven-development` -> `superpowers-test-driven-development`; - `using-git-worktrees` -> `superpowers-using-git-worktrees`; - `using-superpowers` -> `superpowers-using-superpowers`; - `verification-before-completion` -> - `superpowers-verification-before-completion`; `writing-plans` -> - `superpowers-writing-plans`. - -### `hashicorp/agent-skills` - -Source: - -- Skills: - `https://github.com/hashicorp/agent-skills/tree/339a113935812ad75c6ff90d418b739a021826d1/terraform/code-generation/skills` - (commit date: 2026-05-28) - -Managed skills: - -- `terraform-search-import` -> `terraform-terraform-search-import`; - `terraform-test` -> `terraform-terraform-test`. - -### `mattpocock/skills` - -Sources: - -- Engineering skills: - `https://github.com/mattpocock/skills/tree/efa058a349f5ce98b6115bf8b4e0d0ef9c310e0d/skills/engineering` - (commit date: 2026-07-03) -- Productivity skills: - `https://github.com/mattpocock/skills/tree/efa058a349f5ce98b6115bf8b4e0d0ef9c310e0d/skills/productivity` - (commit date: 2026-07-03) - -Managed skills: - -- `code-review` -> `mattpocock-code-review`; `grill-me` -> `grill-me`; - `handoff` -> `mattpocock-handoff` (productivity, compact conversation - handoff for a fresh agent); `research` -> - `mattpocock-research` (engineering, background-agent primary-source - investigation). -- `caveman` -> `mattpocock-caveman` was retired from the managed catalog - by explicit user scope reduction; see `references/external-watchlist.yaml` - for the alert-only retired entry. Do not reimport `caveman` unless the - user re-approves the retained import. - -Approved in-place overrides: - -- `grill-me`: replay `grill-me-bulk-recommended-questions` after each refresh - so the skill asks its initial question set as a dependency-ordered numbered - list with recommendations accepted by default, while still surfacing - contradictions, risks, and unresolved follow-up questions. - -Retired upstream items that were extracted into internal owners are tracked in -the alert-only watchlist owned by `local-agent-sync-external-resources`. - -### `vercel-labs/skills` - -Source: - -- Skills: - `https://github.com/vercel-labs/skills/tree/4ce6d48ac44c8b637db87b2102fea3baca719df1/skills` - (commit date: 2026-07-06) - -Managed skills: - -- `find-skills` -> `vercel-find-skills` (helps users discover and install - agent skills from the open agent skills ecosystem). The local prefix is - `vercel`; keep this prefix narrow to the `vercel-labs/skills` family and - do not reuse it for other imported families. - -### `openai/skills` - -Sources: - -- Curated skills: - `https://github.com/openai/skills/tree/49f948faa9258a0c61caceaf225e179651397431/skills/.curated` - (commit date: 2026-06-23) -- System skills: - `https://github.com/openai/skills/tree/49f948faa9258a0c61caceaf225e179651397431/skills/.system` - (commit date: 2026-06-23) -- Retained document skill: - `https://github.com/openai/skills/tree/45d05d75363abf13f99d09e899d61e07b8010685/skills/.curated/doc` - (commit date: 2026-05-01; absent from current pinned upstream) -- Retained spreadsheet skill: - `https://github.com/openai/skills/tree/e6afb0d74cc75d220df2faf3dd6c635c2dc6a108/skills/.curated/spreadsheet` - (commit date: 2026-04-14; absent from current pinned upstream) -- Retained slides skill: - `https://github.com/openai/skills/tree/e6afb0d74cc75d220df2faf3dd6c635c2dc6a108/skills/.curated/slides` - (commit date: 2026-04-14; absent from current pinned upstream) - -Managed skills: - -- `gh-address-comments` -> `openai-gh-address-comments`; `gh-fix-ci` -> - `openai-gh-fix-ci`; `skill-creator` -> `openai-skill-creator`; `pdf` -> - `openai-pdf`. - -Retained support-only office skills: - -- `doc` -> `openai-docx`; `spreadsheet` -> `openai-spreadsheet`; `slides` -> - `openai-slides`. - -Approved in-place overrides: - -- `openai-docx`: replay `openai-docx-executable-renderer` after each refresh so - the bundled `scripts/render_docx.py` keeps a valid executable shebang and - does not trip repository pre-commit validation. -- `openai-spreadsheet`: replay - `openai-spreadsheet-structured-data-evidence-budget` after each refresh so - large spreadsheet and tabular workflows keep the repository-specific - structured-data evidence budget while preserving full-file correctness checks. - -### `sickn33/antigravity-awesome-skills` - -Source: - -- Skills: - `https://github.com/sickn33/antigravity-awesome-skills/tree/8946c6cdc8468183426d52f85054639b3e1844ae/skills` - (release tag: v13.9.0; commit date: 2026-07-03) - -Managed skills: - -- `api-design-principles` -> `antigravity-api-design-principles`; - `aws-cost-optimizer` -> `antigravity-aws-cost-optimizer`; - `cloudformation-best-practices` -> - `antigravity-cloudformation-best-practices`; `golang-pro` -> - `antigravity-golang-pro`; `grafana-dashboards` -> - `antigravity-grafana-dashboards`; `kubernetes-architect` -> - `antigravity-kubernetes-architect`; `network-engineer` -> - `antigravity-network-engineer`. diff --git a/.github/skills/local-agent-sync-external-resources/references/refresh-execution-guardrails.md b/.github/skills/local-agent-sync-external-resources/references/refresh-execution-guardrails.md deleted file mode 100644 index 1b36e582..00000000 --- a/.github/skills/local-agent-sync-external-resources/references/refresh-execution-guardrails.md +++ /dev/null @@ -1,39 +0,0 @@ -# Refresh Execution Guardrails - -Use this reference before fetching or materializing upstream resources for -`local-sync-external-resources`. - -## Workspace Rule - -Stage upstream repositories outside the repository root. Use -`/private/tmp/cloud-strategy-github-external-refresh` by default for local runs. -Do not clone upstream repositories under repository-root `tmp/`. - -Before graphify or completion reporting, run the bundled workspace guard and -resolve every blocking finding. - -## Discovery Rule - -Start from `references/managed-resource-scope.md`. Build a source-to-local map -for only the declared managed assets before running broad file listing, -repository-wide grep, or graph updates. - -Use sparse or path-limited upstream retrieval when the upstream repository is -large. Fetch only managed skill directories and directly required sibling files. - -## Validation Rule - -Run imported override dry-run after refresh and before updating expected hashes. -Update expected hashes only after the patch is replayable or already applied. - -Use scoped whitespace checks for local governance files and patch files. Do not -rewrite imported upstream content only to satisfy repository whitespace style. - -## Reporting Rule - -Completion reports must state: - -- where upstream snapshots were staged -- whether the workspace guard passed -- whether graphify ran after repo-local refresh leftovers were absent -- which validators ran and which notices remained diff --git a/.github/skills/local-agent-sync-external-resources/references/superpowers-normalization.yaml b/.github/skills/local-agent-sync-external-resources/references/superpowers-normalization.yaml deleted file mode 100644 index b0b2219c..00000000 --- a/.github/skills/local-agent-sync-external-resources/references/superpowers-normalization.yaml +++ /dev/null @@ -1,64 +0,0 @@ -version: 1 -source_family: obra/superpowers -source_ref: d884ae04edebef577e82ff7c4e143debd0bbec99 -local_prefix: superpowers- -blocked_local_prefixes: - - obra- -blocked_managed_reference_prefix: "superpowers:" -managed_skills: - - upstream: brainstorming - legacy_local: obra-brainstorming - local: superpowers-brainstorming - - upstream: dispatching-parallel-agents - legacy_local: obra-dispatching-parallel-agents - local: superpowers-dispatching-parallel-agents - - upstream: executing-plans - legacy_local: obra-executing-plans - local: superpowers-executing-plans - - upstream: finishing-a-development-branch - legacy_local: obra-finishing-a-development-branch - local: superpowers-finishing-a-development-branch - - upstream: receiving-code-review - legacy_local: obra-receiving-code-review - local: superpowers-receiving-code-review - - upstream: requesting-code-review - legacy_local: obra-requesting-code-review - local: superpowers-requesting-code-review - - upstream: subagent-driven-development - legacy_local: obra-subagent-driven-development - local: superpowers-subagent-driven-development - - upstream: systematic-debugging - legacy_local: obra-systematic-debugging - local: superpowers-systematic-debugging - - upstream: test-driven-development - legacy_local: obra-test-driven-development - local: superpowers-test-driven-development - - upstream: using-git-worktrees - legacy_local: obra-using-git-worktrees - local: superpowers-using-git-worktrees - - upstream: using-superpowers - legacy_local: obra-using-superpowers - local: superpowers-using-superpowers - - upstream: verification-before-completion - legacy_local: obra-verification-before-completion - local: superpowers-verification-before-completion - - upstream: writing-plans - legacy_local: obra-writing-plans - local: superpowers-writing-plans -managed_text_replacements: - - from: docs/superpowers - to: tmp/superpowers - reason: Keep retained plans and specs under the repository-local temporary workspace after every superpowers refresh. -live_scan: - include: - - .github/agents - - .github/prompts - - .github/scripts - - .github/skills - - .github/INVENTORY.md - - .github/repo-profiles.yml - - .markdownlint-cli2.jsonc - ignored_files: - - README.md - - CHANGELOG.md - - superpowers-normalization.yaml diff --git a/.github/skills/local-agent-sync-external-resources/scripts/apply_imported_asset_overrides.py b/.github/skills/local-agent-sync-external-resources/scripts/apply_imported_asset_overrides.py deleted file mode 100644 index 3c51c2fa..00000000 --- a/.github/skills/local-agent-sync-external-resources/scripts/apply_imported_asset_overrides.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env python3 -"""Replay registered imported-asset override patches after an upstream refresh.""" - -from __future__ import annotations - -import argparse -import subprocess -import sys -from pathlib import Path - -import yaml - -_SCRIPTS_LIB = Path(__file__).resolve().parents[3] / "scripts" / "lib" -if _SCRIPTS_LIB.parent.as_posix() not in sys.path: - sys.path.insert(0, _SCRIPTS_LIB.parent.as_posix()) - -from lib.repo_paths import find_repo_root - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Apply registered imported-asset override patches." - ) - parser.add_argument( - "--registry", - help="Path to imported-asset-overrides.yaml.", - ) - parser.add_argument( - "--repo-root", - help="Repository root. Defaults to the nearest parent that contains .github/.", - ) - parser.add_argument( - "--id", - action="append", - dest="selected_ids", - help="Apply only the selected override id. Repeatable.", - ) - parser.add_argument( - "--dry-run", - action="store_true", - help="Check patch applicability without writing changes.", - ) - return parser.parse_args() - - -def main() -> int: - args = parse_args() - repo_root = ( - find_repo_root(Path(args.repo_root)) - if args.repo_root - else find_repo_root(Path.cwd()) - ) - skill_root = ( - Path(args.registry).resolve().parent.parent - if args.registry - else Path(__file__).resolve().parent.parent - ) - registry_path = ( - Path(args.registry).resolve() - if args.registry - else skill_root / "references/imported-asset-overrides.yaml" - ) - overrides = load_registry(registry_path) - selected = select_overrides(overrides, args.selected_ids) - if not selected: - print("No imported-asset overrides selected.", file=sys.stderr) - return 1 - - sys.path.insert(0, str((repo_root / ".github/scripts").resolve())) - from lib.fingerprinting import build_fingerprint # pylint: disable=import-error - - for override in selected: - override_id = override["id"] - patch_path = (skill_root / override["patch_path"]).resolve() - target_path = (repo_root / override["target_path"]).resolve() - if not patch_path.is_relative_to(skill_root / "patches"): - print(f"[error] patch path escapes patches/ for {override_id}: {patch_path}", file=sys.stderr) - return 1 - if not target_path.is_relative_to(repo_root): - print(f"[error] target path escapes repo for {override_id}: {target_path}", file=sys.stderr) - return 1 - if not patch_path.is_file(): - print(f"[error] missing patch file for {override_id}: {patch_path}", file=sys.stderr) - return 1 - if not target_path.is_file(): - print(f"[error] missing target file for {override_id}: {target_path}", file=sys.stderr) - return 1 - - apply_strategy = override.get("apply_strategy", "git-apply") - VALID_STRATEGIES = {"git-apply", "git-apply-3way"} - if apply_strategy not in VALID_STRATEGIES: - print(f"[error] unknown apply_strategy for {override_id}: {apply_strategy}", file=sys.stderr) - return 1 - - patch_status = detect_patch_status(repo_root, patch_path, apply_strategy=apply_strategy) - if patch_status == "conflict": - print( - f"[error] patch does not apply cleanly for {override_id}; stop and review the override.", - file=sys.stderr, - ) - return 1 - - if args.dry_run: - if patch_status == "already-applied": - print(f"[dry-run] {override_id}: patch already applied") - elif patch_status == "applicable-with-3way": - print( - f"[dry-run] {override_id}: patch needs 3-way replay " - f"({apply_strategy}) because upstream text changed" - ) - else: - print(f"[dry-run] {override_id}: patch applies cleanly") - continue - - if patch_status == "already-applied": - print(f"[skipped] {override_id}: patch already applied") - continue - - apply_cmd = build_apply_command(patch_path, patch_status) - if run_git(apply_cmd, repo_root) != 0: - print(f"[error] failed to apply patch for {override_id}", file=sys.stderr) - return 1 - - expected_hash = override["expected_content_hash"] - actual_hash = build_fingerprint(repo_root, target_path).content_hash - if actual_hash != expected_hash: - print( - f"[error] patched content hash mismatch for {override_id}: " - f"expected {expected_hash}, got {actual_hash}", - file=sys.stderr, - ) - return 1 - - if patch_status == "applicable-with-3way": - print(f"[applied] {override_id}: {override['target_path']} via 3-way replay") - else: - print(f"[applied] {override_id}: {override['target_path']}") - - return 0 - - -def load_registry(path: Path) -> list[dict[str, str]]: - payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {} - overrides = payload.get("overrides") - if not isinstance(overrides, list): - raise ValueError("Registry must define an overrides list.") - REQUIRED_FIELDS = {"id", "patch_path", "target_path", "apply_strategy", "expected_content_hash"} - normalized: list[dict[str, str]] = [] - for item in overrides: - if not isinstance(item, dict): - raise ValueError("Each override entry must be a mapping.") - missing = REQUIRED_FIELDS - set(item.keys()) - if missing: - raise ValueError(f"Override entry missing required fields: {missing}") - normalized.append({key: str(value) for key, value in item.items()}) - return normalized - - -def select_overrides( - overrides: list[dict[str, str]], selected_ids: list[str] | None -) -> list[dict[str, str]]: - if not selected_ids: - return overrides - selected_set = set(selected_ids) - return [override for override in overrides if override.get("id") in selected_set] - - -def run_git(command: list[str], repo_root: Path, quiet: bool = False) -> int: - try: - completed = subprocess.run( - command, - cwd=repo_root, - text=True, - capture_output=True, - check=False, - timeout=60, - ) - except subprocess.TimeoutExpired: - return -1 - if not quiet and completed.stdout.strip(): - print(completed.stdout.strip()) - if not quiet and completed.stderr.strip(): - print(completed.stderr.strip(), file=sys.stderr) - return completed.returncode - - -def build_apply_command(patch_path: Path, patch_status: str) -> list[str]: - command = ["git", "apply"] - if patch_status == "applicable-with-3way": - command.append("--3way") - command.append(patch_path.as_posix()) - return command - - -def detect_patch_status( - repo_root: Path, - patch_path: Path, - apply_strategy: str = "git-apply", -) -> str: - if run_git(["git", "apply", "--check", patch_path.as_posix()], repo_root, quiet=True) == 0: - return "applicable" - if ( - run_git( - ["git", "apply", "--reverse", "--check", patch_path.as_posix()], - repo_root, - quiet=True, - ) - == 0 - ): - return "already-applied" - if apply_strategy == "git-apply-3way" and ( - run_git( - ["git", "apply", "--3way", "--check", patch_path.as_posix()], - repo_root, - quiet=True, - ) - == 0 - ): - return "applicable-with-3way" - return "conflict" - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/.github/skills/local-agent-sync-external-resources/scripts/check_external_refresh_workspace.py b/.github/skills/local-agent-sync-external-resources/scripts/check_external_refresh_workspace.py deleted file mode 100644 index 18a9acbe..00000000 --- a/.github/skills/local-agent-sync-external-resources/scripts/check_external_refresh_workspace.py +++ /dev/null @@ -1,110 +0,0 @@ -#!/usr/bin/env python3 -"""Validate external-refresh staging before graphify or catalog completion.""" - -from __future__ import annotations - -import argparse -import sys -from pathlib import Path - -REQUIRED_GRAPHIFY_PATTERNS = ("tmp/", "/tmp/", "tmp/external-refresh/", "**/.git/") -REPO_LOCAL_REFRESH_DIRS = ("tmp/external-refresh", "tmp/upstream-refresh") - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Check that external refresh work is staged outside the repository." - ) - parser.add_argument("--repo-root", default=".", help="Repository root.") - parser.add_argument("--workspace", help="External refresh workspace path.") - parser.add_argument( - "--check-graphifyignore", - action="store_true", - help="Require graphify ignore coverage for temporary refresh paths.", - ) - return parser.parse_args() - - -def display_path(root: Path, path: Path) -> str: - try: - return path.resolve().relative_to(root.resolve()).as_posix() - except ValueError: - return path.as_posix() - - -def read_graphifyignore(root: Path) -> set[str]: - path = root / ".graphifyignore" - if not path.is_file(): - return set() - return { - line.strip() - for line in path.read_text(encoding="utf-8").splitlines() - if line.strip() and not line.lstrip().startswith("#") - } - - -def missing_graphifyignore_patterns(root: Path) -> list[str]: - configured = read_graphifyignore(root) - return [pattern for pattern in REQUIRED_GRAPHIFY_PATTERNS if pattern not in configured] - - -def validate_workspace(root: Path, workspace: Path | None) -> list[str]: - if workspace is None: - return [] - root_resolved = root.resolve() - workspace_resolved = workspace.resolve() - if workspace_resolved == root_resolved or workspace_resolved.is_relative_to(root_resolved): - return [ - "External refresh workspace must be outside the repository: " - f"{display_path(root, workspace)}" - ] - return [] - - -def find_repo_local_refresh_dirs(root: Path) -> list[str]: - found: list[str] = [] - tmp_dir = root / "tmp" - if tmp_dir.is_dir(): - for child in tmp_dir.iterdir(): - if child.is_dir() and (child / ".git").exists(): - found.append(child.relative_to(root).as_posix()) - for relative in REPO_LOCAL_REFRESH_DIRS: - path = root / relative - if path.exists() and path.as_posix() not in {f for f in found}: - found.append(relative) - return found - - -def collect_findings( - root: Path, workspace: Path | None, check_graphifyignore: bool -) -> list[str]: - findings: list[str] = [] - findings.extend(validate_workspace(root, workspace)) - leftovers = find_repo_local_refresh_dirs(root) - if leftovers: - findings.append( - "Remove or move repo-local external refresh directories before graphify: " - + ", ".join(leftovers) - ) - if check_graphifyignore: - missing = missing_graphifyignore_patterns(root) - if missing: - findings.append("Missing .graphifyignore refresh patterns: " + ", ".join(missing)) - return findings - - -def main() -> int: - args = parse_args() - root = Path(args.repo_root) - workspace = Path(args.workspace) if args.workspace else None - findings = collect_findings(root, workspace, args.check_graphifyignore) - if findings: - for finding in findings: - print(f"[blocking] {finding}", file=sys.stderr) - return 1 - print("External refresh workspace guard passed.") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/.github/skills/local-agent-sync-external-resources/scripts/normalize_superpowers_imports.py b/.github/skills/local-agent-sync-external-resources/scripts/normalize_superpowers_imports.py deleted file mode 100644 index 4fec0a47..00000000 --- a/.github/skills/local-agent-sync-external-resources/scripts/normalize_superpowers_imports.py +++ /dev/null @@ -1,330 +0,0 @@ -#!/usr/bin/env python3 -"""Normalize imported obra/superpowers assets to local `superpowers-*` ids. - -Dependency decision note: -- Candidates: JSON in the standard library, PyYAML. -- Final choice: PyYAML. -- Why: the repository already uses YAML for sync registries, so the normalizer - can share the catalog reference format without adding a new dependency. -""" - -from __future__ import annotations - -import argparse -import sys -from dataclasses import dataclass -from pathlib import Path - -import yaml - -_SCRIPTS_LIB = Path(__file__).resolve().parents[3] / "scripts" / "lib" -if _SCRIPTS_LIB.parent.as_posix() not in sys.path: - sys.path.insert(0, _SCRIPTS_LIB.parent.as_posix()) - -from lib.repo_paths import find_repo_root - - -DEFAULT_REFERENCE = ( - ".github/skills/local-agent-sync-external-resources/references/superpowers-normalization.yaml" -) -TEXT_SUFFIXES = {".md", ".yaml", ".yml", ".json", ".jsonc", ".py", ".patch"} - - -@dataclass(frozen=True) -class ManagedSkill: - upstream: str - legacy_local: str - local: str - - -@dataclass(frozen=True) -class ManagedPatch: - legacy_path: str - path: str - - -@dataclass(frozen=True) -class ManagedTextReplacement: - old_text: str - new_text: str - - -@dataclass(frozen=True) -class NormalizationConfig: - reference_path: Path - managed_skills: tuple[ManagedSkill, ...] - managed_patches: tuple[ManagedPatch, ...] - managed_text_replacements: tuple[ManagedTextReplacement, ...] - scan_includes: tuple[str, ...] - ignored_files: frozenset[str] - blocked_local_prefixes: tuple[str, ...] = () - blocked_managed_reference_prefixes: tuple[str, ...] = () - - -@dataclass(frozen=True) -class NormalizationChange: - kind: str - path: str - detail: str - - def render(self) -> str: - return f"{self.kind}: {self.path} ({self.detail})" - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Normalize imported obra/superpowers assets to local superpowers-* ids." - ) - parser.add_argument( - "--repo-root", - help="Repository root. Defaults to the nearest parent that contains .github/.", - ) - parser.add_argument( - "--reference", - default=DEFAULT_REFERENCE, - help="Path to superpowers-normalization.yaml, relative to the repository root by default.", - ) - mode = parser.add_mutually_exclusive_group(required=True) - mode.add_argument("--check", action="store_true", help="Report drift without writing changes.") - mode.add_argument("--apply", action="store_true", help="Rewrite paths and text in place.") - return parser.parse_args() - - -def main() -> int: - args = parse_args() - repo_root = find_repo_root(Path(args.repo_root)) if args.repo_root else find_repo_root(Path.cwd()) - reference_path = resolve_reference_path(repo_root, Path(args.reference)) - config = load_config(reference_path) - - if args.check: - changes = detect_drift(repo_root, config) - if not changes: - print("✅ Superpowers import naming is normalized.") - return 0 - print("❌ Superpowers import naming drift found:", file=sys.stderr) - for change in changes: - print(f"- {change.render()}", file=sys.stderr) - return 1 - - try: - changes = apply_normalization(repo_root, config) - except FileExistsError as error: - print(f"❌ {error}", file=sys.stderr) - return 1 - - if not changes: - print("✅ Superpowers import naming was already normalized.") - return 0 - - print(f"✅ Applied {len(changes)} superpowers normalization change(s).") - for change in changes: - print(f"- {change.render()}") - return 0 - - -def resolve_reference_path(repo_root: Path, raw_reference_path: Path) -> Path: - if raw_reference_path.is_absolute(): - return raw_reference_path - return repo_root / raw_reference_path - - -def load_config(reference_path: Path) -> NormalizationConfig: - payload = yaml.safe_load(reference_path.read_text(encoding="utf-8")) or {} - if not isinstance(payload, dict): - raise ValueError("Normalization reference must be a YAML mapping.") - - managed_skills = tuple(load_managed_skills(payload)) - managed_patches = tuple(load_managed_patches(payload)) - managed_text_replacements = tuple(load_managed_text_replacements(payload)) - live_scan = payload.get("live_scan") if isinstance(payload.get("live_scan"), dict) else {} - raw_includes = live_scan.get("include") if isinstance(live_scan, dict) else None - scan_includes = tuple(item for item in raw_includes or () if isinstance(item, str) and item.strip()) - raw_ignored_files = live_scan.get("ignored_files") if isinstance(live_scan, dict) else None - ignored_files = {"README.md", "CHANGELOG.md", reference_path.name} - if isinstance(raw_ignored_files, list): - ignored_files.update(item for item in raw_ignored_files if isinstance(item, str) and item.strip()) - - raw_blocked_local = payload.get("blocked_local_prefixes", []) - blocked_local_prefixes = tuple(item for item in raw_blocked_local if isinstance(item, str) and item.strip()) if isinstance(raw_blocked_local, list) else () - raw_blocked_managed = payload.get("blocked_managed_reference_prefix", []) - blocked_managed_reference_prefixes = tuple(item for item in raw_blocked_managed if isinstance(item, str) and item.strip()) if isinstance(raw_blocked_managed, list) else () - - return NormalizationConfig( - reference_path=reference_path, - managed_skills=managed_skills, - managed_patches=managed_patches, - managed_text_replacements=managed_text_replacements, - scan_includes=scan_includes, - ignored_files=frozenset(ignored_files), - blocked_local_prefixes=blocked_local_prefixes, - blocked_managed_reference_prefixes=blocked_managed_reference_prefixes, - ) - - -def load_managed_skills(payload: dict[str, object]) -> list[ManagedSkill]: - raw_entries = payload.get("managed_skills") - if not isinstance(raw_entries, list): - raise ValueError("Normalization reference must define a managed_skills list.") - - managed_skills: list[ManagedSkill] = [] - for raw_entry in raw_entries: - if not isinstance(raw_entry, dict): - raise ValueError("Each managed skill entry must be a mapping.") - upstream = require_string(raw_entry, "upstream") - legacy_local = require_string(raw_entry, "legacy_local") - local = require_string(raw_entry, "local") - managed_skills.append(ManagedSkill(upstream=upstream, legacy_local=legacy_local, local=local)) - return managed_skills - - -def load_managed_patches(payload: dict[str, object]) -> list[ManagedPatch]: - raw_entries = payload.get("managed_patches", []) - if not isinstance(raw_entries, list): - raise ValueError("managed_patches must be a list when present.") - - managed_patches: list[ManagedPatch] = [] - for raw_entry in raw_entries: - if not isinstance(raw_entry, dict): - raise ValueError("Each managed patch entry must be a mapping.") - legacy_path = require_string(raw_entry, "legacy_path") - path = require_string(raw_entry, "path") - managed_patches.append(ManagedPatch(legacy_path=legacy_path, path=path)) - return managed_patches - - -def load_managed_text_replacements(payload: dict[str, object]) -> list[ManagedTextReplacement]: - raw_entries = payload.get("managed_text_replacements", []) - if not isinstance(raw_entries, list): - raise ValueError("managed_text_replacements must be a list when present.") - - replacements: list[ManagedTextReplacement] = [] - for raw_entry in raw_entries: - if not isinstance(raw_entry, dict): - raise ValueError("Each managed text replacement entry must be a mapping.") - replacements.append( - ManagedTextReplacement( - old_text=require_string(raw_entry, "from"), - new_text=require_string(raw_entry, "to"), - ) - ) - return replacements - - -def require_string(raw_entry: dict[str, object], key: str) -> str: - value = raw_entry.get(key) - if not isinstance(value, str) or not value.strip(): - raise ValueError(f"Missing non-empty `{key}` in normalization reference.") - return value.strip() - - -def detect_drift(repo_root: Path, config: NormalizationConfig) -> list[NormalizationChange]: - changes: list[NormalizationChange] = [] - for old_relative_path, new_relative_path in path_renames(config): - if (repo_root / old_relative_path).exists(): - changes.append( - NormalizationChange("legacy-path", old_relative_path, f"rename to {new_relative_path}") - ) - - for file_path in collect_scan_files(repo_root, config): - relative_path = file_path.relative_to(repo_root).as_posix() - text = file_path.read_text(encoding="utf-8") - for old_text, new_text in text_replacements(config): - if old_text in text: - changes.append(NormalizationChange("legacy-reference", relative_path, f"{old_text} -> {new_text}")) - - return changes - - -def _safe_replace(text: str, old: str, new: str) -> str: - import re - if old.startswith("superpowers:") or old.startswith("obra-"): - pattern = re.compile(r'(? list[NormalizationChange]: - changes: list[NormalizationChange] = [] - for old_relative_path, new_relative_path in path_renames(config): - old_path = repo_root / old_relative_path - new_path = repo_root / new_relative_path - if not old_path.exists(): - continue - if new_path.exists(): - raise FileExistsError( - f"Cannot rename {old_relative_path} to {new_relative_path}: target already exists." - ) - new_path.parent.mkdir(parents=True, exist_ok=True) - old_path.rename(new_path) - changes.append(NormalizationChange("renamed-path", old_relative_path, f"to {new_relative_path}")) - - replacements = text_replacements(config) - for file_path in collect_scan_files(repo_root, config): - original_text = file_path.read_text(encoding="utf-8") - updated_text = original_text - applied_replacements: list[str] = [] - for old_text, new_text in replacements: - if old_text not in updated_text: - continue - updated_text = _safe_replace(updated_text, old_text, new_text) - applied_replacements.append(f"{old_text} -> {new_text}") - if updated_text == original_text: - continue - file_path.write_text(updated_text, encoding="utf-8") - relative_path = file_path.relative_to(repo_root).as_posix() - changes.append(NormalizationChange("updated-text", relative_path, "; ".join(applied_replacements))) - - return changes - - -def path_renames(config: NormalizationConfig) -> list[tuple[str, str]]: - skill_renames = [ - (f".github/skills/{entry.legacy_local}", f".github/skills/{entry.local}") - for entry in config.managed_skills - ] - patch_root = config.reference_path.parent.parent.relative_to(find_repo_root(config.reference_path)) - patch_renames = [ - ((patch_root / entry.legacy_path).as_posix(), (patch_root / entry.path).as_posix()) - for entry in config.managed_patches - ] - return skill_renames + patch_renames - - -def text_replacements(config: NormalizationConfig) -> list[tuple[str, str]]: - replacements: list[tuple[str, str]] = [] - for entry in config.managed_skills: - replacements.append((entry.legacy_local, entry.local)) - replacements.append((f"superpowers:{entry.upstream}", entry.local)) - for entry in config.managed_patches: - replacements.append((entry.legacy_path, entry.path)) - for entry in config.managed_text_replacements: - replacements.append((entry.old_text, entry.new_text)) - return replacements - - -def collect_scan_files(repo_root: Path, config: NormalizationConfig) -> list[Path]: - paths: set[Path] = set() - for include in config.scan_includes: - candidate = repo_root / include - if candidate.is_file() and should_scan(candidate, repo_root, config): - paths.add(candidate) - continue - if not candidate.is_dir(): - continue - for child in candidate.rglob("*"): - if child.is_file() and should_scan(child, repo_root, config): - paths.add(child) - return sorted(paths) - - -def should_scan(path: Path, repo_root: Path, config: NormalizationConfig) -> bool: - relative_parts = path.relative_to(repo_root).parts - if path.name in config.ignored_files: - return False - if "__pycache__" in relative_parts or ".venv" in relative_parts: - return False - return path.suffix in TEXT_SUFFIXES - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/.github/skills/local-agent-sync-external-resources/scripts/sync_resource_fingerprint.py b/.github/skills/local-agent-sync-external-resources/scripts/sync_resource_fingerprint.py deleted file mode 100644 index b34cb9dc..00000000 --- a/.github/skills/local-agent-sync-external-resources/scripts/sync_resource_fingerprint.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env python3 -"""Build and diff deterministic resource fingerprints for sync workflows.""" - -from __future__ import annotations - -import argparse -import json -from pathlib import Path -import sys - -_SCRIPTS_LIB = Path(__file__).resolve().parents[3] / "scripts" / "lib" -if _SCRIPTS_LIB.parent.as_posix() not in sys.path: - sys.path.insert(0, _SCRIPTS_LIB.parent.as_posix()) - -from lib.repo_paths import find_repo_root - -REPO_ROOT = find_repo_root(Path(__file__).resolve()) -SCRIPTS_ROOT = REPO_ROOT / ".github/scripts" -DEFAULT_SNAPSHOT_OUTPUT = REPO_ROOT / "tmp/superpowers/internal-agent-sync-control-center.manifest.json" -if SCRIPTS_ROOT.as_posix() not in sys.path: - sys.path.insert(0, SCRIPTS_ROOT.as_posix()) - -from lib.fingerprinting import build_manifest, collect_files, diff_manifests, load_manifest, render_diff_text - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser(description=__doc__) - subparsers = parser.add_subparsers(dest="command", required=True) - - snapshot = subparsers.add_parser("snapshot", help="Generate a fingerprint manifest for files or directories.") - snapshot.add_argument("paths", nargs="+", help="Files or directories to fingerprint.") - snapshot.add_argument("--root", default=".", help="Repository root used for relative paths.") - snapshot.add_argument( - "--output", - default=DEFAULT_SNAPSHOT_OUTPUT.as_posix(), - help="Path to write the JSON manifest. Defaults to tmp/superpowers/internal-agent-sync-control-center.manifest.json.", - ) - snapshot.add_argument("--source-ref-base", help="Optional prefix used to build source_ref values from relative paths.") - - diff = subparsers.add_parser("diff", help="Compare two fingerprint manifests.") - diff.add_argument("--old", required=True, help="Baseline manifest path.") - diff.add_argument("--new", required=True, help="Candidate manifest path.") - diff.add_argument("--format", choices=["text", "json"], default="text", help="Output format.") - - return parser.parse_args() - - -def main() -> int: - args = parse_args() - if args.command == "snapshot": - return run_snapshot(args) - return run_diff(args) - - -def run_snapshot(args: argparse.Namespace) -> int: - try: - root = Path(args.root).resolve() - files = collect_files(root, [Path(path) for path in args.paths]) - manifest = build_manifest(root, files, source_ref_base=args.source_ref_base) - output_path = Path(args.output) - output_path.parent.mkdir(parents=True, exist_ok=True) - output_path.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8") - print(output_path.as_posix()) - return 0 - except FileNotFoundError as exc: - print(f"[error] {exc}", file=sys.stderr) - return 1 - except json.JSONDecodeError as exc: - print(f"[error] manifest parse error: {exc}", file=sys.stderr) - return 1 - - -def run_diff(args: argparse.Namespace) -> int: - try: - old_manifest = load_manifest(Path(args.old)) - new_manifest = load_manifest(Path(args.new)) - result = diff_manifests(old_manifest, new_manifest) - if args.format == "json": - print(json.dumps(result, indent=2, sort_keys=True)) - return 0 - - print(render_diff_text(result)) - return 0 - except FileNotFoundError as exc: - print(f"[error] {exc}", file=sys.stderr) - return 1 - except json.JSONDecodeError as exc: - print(f"[error] manifest parse error: {exc}", file=sys.stderr) - return 1 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_apply_overrides.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_apply_overrides.py deleted file mode 100644 index e5c2cc3f..00000000 --- a/tests/github/skills/local-agent-sync-external-resources/scripts/test_apply_overrides.py +++ /dev/null @@ -1,80 +0,0 @@ -import subprocess -import sys -from pathlib import Path -from unittest.mock import patch - -import pytest - -REPO_ROOT = next( - parent for parent in Path(__file__).resolve().parents - if (parent / "AGENTS.md").exists() and (parent / ".github").exists() -) -SCRIPT_DIR = ( - REPO_ROOT / ".github/skills/local-agent-sync-external-resources/scripts" -) -sys.path.insert(0, SCRIPT_DIR.as_posix()) - -from apply_imported_asset_overrides import ( # noqa: E402 - load_registry, - select_overrides, - detect_patch_status, -) - - -def test_load_registry_validates_required_fields(tmp_path: Path) -> None: - registry = tmp_path / "registry.yaml" - registry.write_text( - "overrides:\n" - " - id: test\n" - " patch_path: patches/test.patch\n" - " target_path: .github/skills/test/SKILL.md\n" - " apply_strategy: git-apply\n" - " expected_content_hash: abc123\n", - encoding="utf-8", - ) - result = load_registry(registry) - assert len(result) == 1 - assert result[0]["id"] == "test" - - -def test_load_registry_rejects_non_list_overrides(tmp_path: Path) -> None: - registry = tmp_path / "registry.yaml" - registry.write_text("overrides: not-a-list\n", encoding="utf-8") - with pytest.raises(ValueError, match="overrides list"): - load_registry(registry) - - -def test_load_registry_rejects_non_mapping_entries(tmp_path: Path) -> None: - registry = tmp_path / "registry.yaml" - registry.write_text("overrides:\n - just-a-string\n", encoding="utf-8") - with pytest.raises(ValueError, match="mapping"): - load_registry(registry) - - -def test_select_overrides_filters_by_id() -> None: - overrides = [ - {"id": "a", "patch_path": "a.patch"}, - {"id": "b", "patch_path": "b.patch"}, - ] - assert len(select_overrides(overrides, ["a"])) == 1 - assert select_overrides(overrides, None) == overrides - - -def test_detect_patch_status_returns_conflict_for_bad_patch(tmp_path: Path) -> None: - repo_root = tmp_path / "repo" - repo_root.mkdir() - subprocess.run(["git", "init", "-q"], cwd=repo_root, check=True, timeout=10) - subprocess.run(["git", "config", "user.email", "t@t.com"], cwd=repo_root, check=True, timeout=10) - subprocess.run(["git", "config", "user.name", "t"], cwd=repo_root, check=True, timeout=10) - (repo_root / "file.txt").write_text("content\n", encoding="utf-8") - subprocess.run(["git", "add", "."], cwd=repo_root, check=True, timeout=10) - subprocess.run(["git", "commit", "-qm", "init"], cwd=repo_root, check=True, timeout=10) - - bad_patch = tmp_path / "bad.patch" - bad_patch.write_text( - "--- a/nonexistent.txt\n+++ b/nonexistent.txt\n@@ -1 +1 @@\n-old\n+new\n", - encoding="utf-8", - ) - - status = detect_patch_status(repo_root, bad_patch) - assert status == "conflict" diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_normalize.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_normalize.py deleted file mode 100644 index 2ed5a60e..00000000 --- a/tests/github/skills/local-agent-sync-external-resources/scripts/test_normalize.py +++ /dev/null @@ -1,54 +0,0 @@ -import sys -from pathlib import Path - -import pytest - -REPO_ROOT = next( - parent for parent in Path(__file__).resolve().parents - if (parent / "AGENTS.md").exists() and (parent / ".github").exists() -) -SCRIPT_DIR = ( - REPO_ROOT / ".github/skills/local-agent-sync-external-resources/scripts" -) -sys.path.insert(0, SCRIPT_DIR.as_posix()) - -from normalize_superpowers_imports import ( # noqa: E402 - load_config, - detect_drift, - NormalizationConfig, - ManagedSkill, -) - - -def test_load_config_validates_managed_skills(tmp_path: Path) -> None: - ref = tmp_path / "ref.yaml" - ref.write_text("managed_skills: not-a-list\n", encoding="utf-8") - with pytest.raises(ValueError, match="managed_skills"): - load_config(ref) - - -def test_detect_drift_finds_legacy_path(tmp_path: Path) -> None: - repo_root = tmp_path / "repo" - repo_root.mkdir() - (repo_root / ".github" / "skills" / "obra-brainstorming").mkdir(parents=True) - (repo_root / ".github" / "skills" / "obra-brainstorming" / "SKILL.md").write_text( - "# old\n", encoding="utf-8" - ) - ref_dir = repo_root / ".github" / "skills" / "local-agent-sync-external-resources" / "references" - ref_dir.mkdir(parents=True) - ref = ref_dir / "ref.yaml" - ref.write_text("managed_skills: []\n", encoding="utf-8") - config = NormalizationConfig( - reference_path=ref, - managed_skills=(ManagedSkill( - upstream="brainstorming", - legacy_local="obra-brainstorming", - local="superpowers-brainstorming", - ),), - managed_patches=(), - managed_text_replacements=(), - scan_includes=(".github/skills",), - ignored_files=frozenset(), - ) - changes = detect_drift(repo_root, config) - assert any(c.kind == "legacy-path" for c in changes) diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_workspace_check.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_workspace_check.py deleted file mode 100644 index 0396d1e1..00000000 --- a/tests/github/skills/local-agent-sync-external-resources/scripts/test_workspace_check.py +++ /dev/null @@ -1,51 +0,0 @@ -import sys -from pathlib import Path - -import pytest - -REPO_ROOT = next( - parent for parent in Path(__file__).resolve().parents - if (parent / "AGENTS.md").exists() and (parent / ".github").exists() -) -SCRIPT_DIR = ( - REPO_ROOT / ".github/skills/local-agent-sync-external-resources/scripts" -) -sys.path.insert(0, SCRIPT_DIR.as_posix()) - -from check_external_refresh_workspace import ( # noqa: E402 - validate_workspace, - find_repo_local_refresh_dirs, -) - - -def test_validate_workspace_blocks_inside_repo(tmp_path: Path) -> None: - workspace = tmp_path / "subdir" - workspace.mkdir() - findings = validate_workspace(tmp_path, workspace) - assert len(findings) == 1 - assert "outside the repository" in findings[0] - - -def test_validate_workspace_allows_outside_repo(tmp_path: Path) -> None: - workspace = tmp_path / "external-workspace" - workspace.mkdir() - repo = tmp_path / "repo" - repo.mkdir() - findings = validate_workspace(repo, workspace) - assert findings == [] - - -def test_validate_workspace_no_check_when_none(tmp_path: Path) -> None: - findings = validate_workspace(tmp_path, None) - assert findings == [] - - -def test_find_repo_local_refresh_dirs_detects_leftovers(tmp_path: Path) -> None: - (tmp_path / "tmp" / "external-refresh").mkdir(parents=True) - dirs = find_repo_local_refresh_dirs(tmp_path) - assert "tmp/external-refresh" in dirs - - -def test_find_repo_local_refresh_dirs_empty_when_clean(tmp_path: Path) -> None: - dirs = find_repo_local_refresh_dirs(tmp_path) - assert dirs == [] From 3e71927d8e531b0c1e7d749dba82736b4837651a Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 13:21:32 +0200 Subject: [PATCH 36/59] refactor: update internal review code descriptions for clarity and consistency --- .github/skills/internal-review-code/SKILL.md | 152 ++++-------------- .../internal-review-code/agents/openai.yaml | 4 +- 2 files changed, 37 insertions(+), 119 deletions(-) diff --git a/.github/skills/internal-review-code/SKILL.md b/.github/skills/internal-review-code/SKILL.md index 77e34e6b..ad12b978 100644 --- a/.github/skills/internal-review-code/SKILL.md +++ b/.github/skills/internal-review-code/SKILL.md @@ -1,137 +1,55 @@ --- name: internal-review-code -description: Use when review evidence needs line-level or language-specific defect checks for Python, Bash, Terraform, Java, or Node.js/TypeScript code, tests, or scripts. +description: Use when reviewing a branch, pull request, work-in-progress diff, or code changes since a fixed point against repository standards and an originating spec. --- # Internal Review Code ## Referenced skills -- `mattpocock-code-review`: imported two-axis review core (Standards + Spec) for diff-based parallel sub-agent review. -- `internal-review-high-level`: systems-level review beyond line-level defects. -- `internal-python`: Python anti-pattern depth (load the review anti-patterns reference from that skill). -- `internal-bash`: Bash anti-pattern depth (load the review anti-patterns reference from that skill). -- `internal-terraform`: Terraform anti-pattern depth (load the review anti-patterns reference from that skill). -- `internal-java`: Java anti-pattern depth (load the review anti-patterns reference from that skill). -- `internal-nodejs`: Node.js anti-pattern depth (load the review anti-patterns reference from that skill). -- `superpowers-verification-before-completion`: evidence gate before claiming no findings or merge readiness. +- `mattpocock-code-review`: operating core for the fixed-point Standards and Spec review. ## When to use -- Perform a line-level code review on Python, Bash, Terraform, Java, or Node.js/TypeScript files. -- Provide structured findings with per-language anti-pattern detection. -- The diff or code change is the primary review surface. +Use for a branch, pull request, work-in-progress diff, or code change that must +be reviewed against repository standards and its originating spec. -## When not to use +## Core contract -- The primary target is an AI resource, workflow, policy, plan, or documentation package; use `internal-gateway-review` instead. -- The review needs systems-level architectural analysis; use `internal-review-high-level` instead. -- The user asks for a two-axis Standards + Spec parallel review; consult `mattpocock-code-review` as the imported core. +Use `mattpocock-code-review` as the complete review engine and follow its +process end to end. This wrapper provides the stable repository-owned entrypoint +and the local context below; it does not redefine the core's axes, process, or +output. -## Role +## Repository context -`internal-review-code` is the only repo-owned line-level review contract. It owns trigger, boundary, severity, output shape, and validation discipline. Language-specific anti-pattern depth lives in the nearest language owner, not in this wrapper. +- Treat `AGENTS.md` and any narrower owner instructions that govern the changed + files as Standards sources. +- Apply repository precedence when standards conflict: direct user instructions, + then the nearest owner, then broader repository policy. +- If `docs/agents/issue-tracker.md` is absent, do not assume an issue-tracker + integration. Continue through the core's other Spec sources, then use its + no-spec path when none is available. +- Keep Standards and Spec findings separate as required by the core. -## Standalone quick start +## Boundaries -When this skill is used directly instead of through `internal-gateway-review`, establish these inputs first: - -- the review question or requested scope -- the changed files or diff being reviewed -- the validation already run and the main evidence gaps that remain - -Then produce: - -- findings grouped by severity -- a file path and line reference for every finding -- a short residual-risk or unverified-area summary - -## Context checklist - -Establish these review inputs before grading the diff: - -- What behavior, requirement, or defect is the change trying to address? -- Which files, tests, or runtime paths carry the change? -- Are there rollout, backward-compatibility, or migration constraints? -- What is the expected validation path, and what is still unverified? - -## Severity levels - -| Level | Meaning | Action | -| --- | --- | --- | -| `Critical` | Security flaw, data loss risk, or correctness bug | Must fix before merge | -| `Major` | High-risk maintainability issue or deviation from mandatory rules | Should fix before merge | -| `Minor` | Improvement that reduces technical debt or improves clarity | Fix recommended | -| `Nit` | Style inconsistency, naming preference, or cosmetic issue | Fix optional but encouraged | -| `Notes` | Assumptions, open questions, or follow-up suggestions | Informational only | - -## Escalation rules - -- Any single anti-pattern repeated three or more times in the same diff escalates one severity level. -- Any deviation from the matching language skill baseline is at minimum a `Nit`. -- Any violation of `security-baseline.md` is at minimum a `Major`. - -## Cross-language checks - -| Severity | Check | -| --- | --- | -| `Critical` | Hardcoded secrets, tokens, passwords, or API keys | -| `Major` | Missing input validation on external inputs | -| `Major` | Missing error handling on I/O operations | -| `Minor` | Non-English comments, logs, or error messages | -| `Minor` | TODO/FIXME/HACK without linked issue or ticket | -| `Nit` | Trailing whitespace or inconsistent EOF newlines | - -## Review lenses - -Always cover these dimensions: - -- Functionality: correctness, edge cases, failure handling, and requirement fit -- Security: input validation, secret handling, privilege boundaries, unsafe interpolation, and dependency risk -- Performance: unnecessary loops, repeated work, hot-path regressions, or avoidable I/O -- Tests: meaningful coverage, edge-case coverage, and whether the validation actually exercises the changed behavior -- Maintainability: naming, cohesion, complexity, dead code, and local convention fit - -## Simplification rubric - -When the review also asks whether the change can be simplified safely, use this rubric and keep every recommendation behavior-preserving: - -- Reuse: prefer an existing helper or shared abstraction over new near-duplicate logic. -- Quality: flag redundant state, parameter sprawl, copy-paste branches, and stringly-typed values when a stronger local contract already exists. -- Efficiency: flag repeated work, duplicate reads, unnecessary recomputation, and overly broad scans that add cost without benefit. -- Clarity: flag deep nesting, weak naming, dead code, redundant comments, and indirection that no longer earns its keep. - -Only elevate simplification suggestions when they materially improve maintainability, correctness, or cost. - -## Review workflow - -1. **Identify languages and changed surfaces** in the diff. -2. **Read enough nearby context** to understand intent, requirements, and test strategy. -3. **Load applicable anti-pattern references** from the nearest language owner. -4. **Scan each changed file** against the relevant anti-pattern reference. -5. **Cross-check the review lenses** for functionality, security, performance, tests, and maintainability. -6. **Self-question each finding**: Is this really wrong, or am I misunderstanding the context? -7. **Apply escalation rules** for repeated violations. -8. **Group findings** by severity: `Critical` -> `Major` -> `Minor` -> `Nit` -> `Notes`. -9. **Include file path and line reference** for every finding. -10. **Suggest a concrete fix** for each finding. -11. **Summarize** total finding count per severity at the end. - -## Delegation - -Stay with `internal-review-code` when the main need is defect-first review across mixed Python, Bash, Terraform, Java, or Node.js changes. - -- Add `mattpocock-code-review` when the user explicitly asks for a two-axis Standards + Spec parallel review. -- Add `internal-terraform` when the review is primarily about Terraform resource modeling, module interfaces, or drift-safe HCL changes. -- Add `antigravity-golang-pro` when the review is primarily about Go concurrency, service design, or Go performance behavior. -- Add `awesome-copilot-codeql` when the review is primarily about CodeQL workflow setup or SARIF handling. -- Add `awesome-copilot-secret-scanning` when the review is primarily about GitHub-native secret scanning or push protection. -- Add language or domain specialists only when they materially improve the finding quality. +- Review the diff; do not apply fixes unless the user asks in a separate step. +- Do not load implementation-language skills or systems-level review skills + merely because their file types or topics appear in the diff. +- Do not add another severity model, review lens set, workflow, or output + template around the core. +- If the fixed point is missing, invalid, or produces an empty diff, stop at the + core preflight. +- If no spec exists, follow the core's no-spec path and state the evidence gap. ## Validation -- Verify every finding references a real file path and line from the diff. -- Verify severity assignments match the anti-pattern reference rules. -- Verify escalation rules are applied for repeated violations. -- Verify cross-language checks are applied regardless of primary language. -- Use `superpowers-verification-before-completion` before claiming there are no findings, the review is complete, or the change is merge-ready. +Before reporting the review, confirm that: + +- the fixed point resolved and the reviewed diff is the intended non-empty diff; +- each Standards finding cites the governing repository rule or is labelled as + a judgement-call smell; +- each Spec finding cites its source, or the report states that no spec was + available; +- the two core axes remain separate in the final report. diff --git a/.github/skills/internal-review-code/agents/openai.yaml b/.github/skills/internal-review-code/agents/openai.yaml index b7a41c74..daa9fb33 100644 --- a/.github/skills/internal-review-code/agents/openai.yaml +++ b/.github/skills/internal-review-code/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Internal Review Code" - short_description: "Line-level code review wrapper" - default_prompt: "Use $internal-review-code for line-level review of code changes; load language anti-pattern references from the nearest language owner as needed." + short_description: "Repository wrapper for two-axis code review" + default_prompt: "Use $internal-review-code as the repository wrapper and follow $mattpocock-code-review as its review core." From d8eaf10ab92d85321033f904ee66970c971a8a9b Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 13:30:55 +0200 Subject: [PATCH 37/59] feat: implement internal AI resource review framework with supporting documentation and tests --- .github/prompts/internal-review-ai-resources.prompt.md | 4 ++-- .../SKILL.md | 0 .../agents/openai.yaml | 0 .../references/report-contract.md | 0 .../references/review-checklist.md | 0 .../references/review-profiles.md | 0 .../references/review-usefulness-replay-fixture.md | 0 .../test_report_contract.py | 0 8 files changed, 2 insertions(+), 2 deletions(-) rename .github/skills/{internal-ai-resource-review => internal-review-ai-resources}/SKILL.md (100%) rename .github/skills/{internal-ai-resource-review => internal-review-ai-resources}/agents/openai.yaml (100%) rename .github/skills/{internal-ai-resource-review => internal-review-ai-resources}/references/report-contract.md (100%) rename .github/skills/{internal-ai-resource-review => internal-review-ai-resources}/references/review-checklist.md (100%) rename .github/skills/{internal-ai-resource-review => internal-review-ai-resources}/references/review-profiles.md (100%) rename .github/skills/{internal-ai-resource-review => internal-review-ai-resources}/references/review-usefulness-replay-fixture.md (100%) rename tests/github/skills/{internal-ai-resource-review => internal-review-ai-resources}/test_report_contract.py (100%) diff --git a/.github/prompts/internal-review-ai-resources.prompt.md b/.github/prompts/internal-review-ai-resources.prompt.md index 03ad1fb2..28fbeb82 100644 --- a/.github/prompts/internal-review-ai-resources.prompt.md +++ b/.github/prompts/internal-review-ai-resources.prompt.md @@ -35,9 +35,9 @@ Use these repository sources first: - [.github/INVENTORY.md](../INVENTORY.md) - [INTERNAL_CONTRACT.md](../../INTERNAL_CONTRACT.md) - [.github/agents/internal-gateway-review.agent.md](../agents/internal-gateway-review.agent.md) -- [.github/skills/internal-ai-resource-review/SKILL.md](../skills/internal-ai-resource-review/SKILL.md) +- [.github/skills/internal-review-ai-resources/SKILL.md](../skills/internal-review-ai-resources/SKILL.md) -Then use `internal-ai-resource-review` as the reusable qualitative owner for +Then use `internal-review-ai-resources` as the reusable qualitative owner for profile selection, target coverage, lifecycle checks, report shape, and bundle review depth. diff --git a/.github/skills/internal-ai-resource-review/SKILL.md b/.github/skills/internal-review-ai-resources/SKILL.md similarity index 100% rename from .github/skills/internal-ai-resource-review/SKILL.md rename to .github/skills/internal-review-ai-resources/SKILL.md diff --git a/.github/skills/internal-ai-resource-review/agents/openai.yaml b/.github/skills/internal-review-ai-resources/agents/openai.yaml similarity index 100% rename from .github/skills/internal-ai-resource-review/agents/openai.yaml rename to .github/skills/internal-review-ai-resources/agents/openai.yaml diff --git a/.github/skills/internal-ai-resource-review/references/report-contract.md b/.github/skills/internal-review-ai-resources/references/report-contract.md similarity index 100% rename from .github/skills/internal-ai-resource-review/references/report-contract.md rename to .github/skills/internal-review-ai-resources/references/report-contract.md diff --git a/.github/skills/internal-ai-resource-review/references/review-checklist.md b/.github/skills/internal-review-ai-resources/references/review-checklist.md similarity index 100% rename from .github/skills/internal-ai-resource-review/references/review-checklist.md rename to .github/skills/internal-review-ai-resources/references/review-checklist.md diff --git a/.github/skills/internal-ai-resource-review/references/review-profiles.md b/.github/skills/internal-review-ai-resources/references/review-profiles.md similarity index 100% rename from .github/skills/internal-ai-resource-review/references/review-profiles.md rename to .github/skills/internal-review-ai-resources/references/review-profiles.md diff --git a/.github/skills/internal-ai-resource-review/references/review-usefulness-replay-fixture.md b/.github/skills/internal-review-ai-resources/references/review-usefulness-replay-fixture.md similarity index 100% rename from .github/skills/internal-ai-resource-review/references/review-usefulness-replay-fixture.md rename to .github/skills/internal-review-ai-resources/references/review-usefulness-replay-fixture.md diff --git a/tests/github/skills/internal-ai-resource-review/test_report_contract.py b/tests/github/skills/internal-review-ai-resources/test_report_contract.py similarity index 100% rename from tests/github/skills/internal-ai-resource-review/test_report_contract.py rename to tests/github/skills/internal-review-ai-resources/test_report_contract.py From e338c97052bf75317ec122c3fa8fbc118f4b57e2 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 13:31:57 +0200 Subject: [PATCH 38/59] refactor: rename internal AI resource review skill and update references --- .../skills/internal-review-ai-resources/SKILL.md | 14 +++++++------- .../agents/openai.yaml | 4 ++-- .../test_report_contract.py | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/skills/internal-review-ai-resources/SKILL.md b/.github/skills/internal-review-ai-resources/SKILL.md index c209e5e4..e56340ad 100644 --- a/.github/skills/internal-review-ai-resources/SKILL.md +++ b/.github/skills/internal-review-ai-resources/SKILL.md @@ -1,9 +1,9 @@ --- -name: internal-ai-resource-review +name: internal-review-ai-resources description: Use when reviewing repository-owned AI resources, bundle siblings, catalog workflows, or retained AI review packages before deciding keep, patch, split, compress, or retire actions. --- -# Internal AI Resource Review +# Internal Review AI Resources ## Referenced skills @@ -11,11 +11,11 @@ This index lists every other skill that this file asks the agent to load, route - `internal-copilot-audit`: on-demand drift lens for overlap, hollow references, stale contracts, naming drift, and governance drift. -Use this skill as the qualitative review owner for repository-owned AI resources -and retained AI review packages. It owns profile selection, evidence-first -qualitative review, lifecycle checks, and proportional reporting. Keep -`internal-copilot-audit` as the drift lens instead of copying its audit order -here. +Use this skill as the qualitative review owner for repository-owned AI +resources and retained AI review packages. It owns profile selection, +evidence-first qualitative review, lifecycle checks, and proportional +reporting. Keep `internal-copilot-audit` as the drift lens instead of copying +its audit order here. ## When to use diff --git a/.github/skills/internal-review-ai-resources/agents/openai.yaml b/.github/skills/internal-review-ai-resources/agents/openai.yaml index 81cdb8a4..08545262 100644 --- a/.github/skills/internal-review-ai-resources/agents/openai.yaml +++ b/.github/skills/internal-review-ai-resources/agents/openai.yaml @@ -1,4 +1,4 @@ interface: - display_name: "Internal AI Resource Review" + display_name: "Internal Review AI Resources" short_description: "Review internal AI resource bundles" - default_prompt: "Use $internal-ai-resource-review to assess an AI resource, bundle, or catalog path and recommend keep, patch, split, or retire actions." + default_prompt: "Use $internal-review-ai-resources to assess an AI resource, bundle, or catalog path and recommend keep, patch, split, or retire actions." diff --git a/tests/github/skills/internal-review-ai-resources/test_report_contract.py b/tests/github/skills/internal-review-ai-resources/test_report_contract.py index 0d5f67c2..35f6f965 100644 --- a/tests/github/skills/internal-review-ai-resources/test_report_contract.py +++ b/tests/github/skills/internal-review-ai-resources/test_report_contract.py @@ -6,14 +6,14 @@ for parent in Path(__file__).resolve().parents if (parent / "AGENTS.md").exists() and (parent / ".github").exists() ) -SKILL_PATH = REPO_ROOT / ".github/skills/internal-ai-resource-review/SKILL.md" +SKILL_PATH = REPO_ROOT / ".github/skills/internal-review-ai-resources/SKILL.md" REPORT_CONTRACT_PATH = ( REPO_ROOT - / ".github/skills/internal-ai-resource-review/references/report-contract.md" + / ".github/skills/internal-review-ai-resources/references/report-contract.md" ) REVIEW_PROFILES_PATH = ( REPO_ROOT - / ".github/skills/internal-ai-resource-review/references/review-profiles.md" + / ".github/skills/internal-review-ai-resources/references/review-profiles.md" ) From 63af21d54a604a0b15da7ebaaee5d361908cd2b1 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 13:32:56 +0200 Subject: [PATCH 39/59] refactor: update skill inventory and references for internal review resources --- .github/INVENTORY.md | 2 +- .github/scripts/lib/token_risks.py | 2 +- .../references/writing-skills-checklist.md | 10 +++++++ .../patches/grill-me.patch | 7 ++--- .../patches/openai-docx-render-docx.patch | 1 + .../patches/openai-spreadsheet.patch | 10 ++++++- .../patches/superpowers-brainstorming.patch | 26 ++++++++++++++----- .../references/managed-resources.yaml | 4 +-- .../test_internal_skill_creator_contract.py | 22 ++++++++++++++++ 9 files changed, 69 insertions(+), 15 deletions(-) diff --git a/.github/INVENTORY.md b/.github/INVENTORY.md index 83384bcb..64f7c8dd 100644 --- a/.github/INVENTORY.md +++ b/.github/INVENTORY.md @@ -57,7 +57,6 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/internal-agent-creator/SKILL.md` - `.github/skills/internal-agent-support-lane-change-engine/SKILL.md` - `.github/skills/internal-agent-support-next-step/SKILL.md` -- `.github/skills/internal-ai-resource-review/SKILL.md` - `.github/skills/internal-aws-governance/SKILL.md` - `.github/skills/internal-aws-lambda/SKILL.md` - `.github/skills/internal-aws-mcp-research/SKILL.md` @@ -113,6 +112,7 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/internal-python-project/SKILL.md` - `.github/skills/internal-python-script/SKILL.md` - `.github/skills/internal-python/SKILL.md` +- `.github/skills/internal-review-ai-resources/SKILL.md` - `.github/skills/internal-review-code/SKILL.md` - `.github/skills/internal-review-high-level/SKILL.md` - `.github/skills/internal-skill-creator/SKILL.md` diff --git a/.github/scripts/lib/token_risks.py b/.github/scripts/lib/token_risks.py index 028c65ee..e09c9787 100644 --- a/.github/scripts/lib/token_risks.py +++ b/.github/scripts/lib/token_risks.py @@ -153,7 +153,7 @@ def check_delegated_review_prompt_budget(root: Path) -> list[Finding]: ), suggestion=( "Keep user inputs and the analysis-only boundary in the prompt, but move reusable workflow " - "detail into .github/skills/internal-ai-resource-review/." + "detail into .github/skills/internal-review-ai-resources/." ), ) ] diff --git a/.github/skills/internal-skill-creator/references/writing-skills-checklist.md b/.github/skills/internal-skill-creator/references/writing-skills-checklist.md index feb7eed7..97b5e0e5 100644 --- a/.github/skills/internal-skill-creator/references/writing-skills-checklist.md +++ b/.github/skills/internal-skill-creator/references/writing-skills-checklist.md @@ -39,6 +39,16 @@ This is a local distilled checklist informed by external skill-authoring guidanc - Do not rewrite a long body just because it is long. Rewrite only when it duplicates the route, duplicates another owner, or keeps reference material inline. - A clean "tighten" outcome is often better than expanding the skill. +## Core-backed wrappers + +- Compare the wrapper against its core before editing. Inventory which responsibilities the core already owns and remove local restatements instead of polishing them. +- Keep the wrapper limited to its retrieval trigger, repository-local policy, and proven environment fallbacks that the core cannot know. +- Do not restate the core's workflow, decision logic, output contract, or validation procedure. Reference the core by skill name and owner behavior. +- Check the wrapper `SKILL.md`, `agents/openai.yaml`, paired agent, focused tests, and nearby routing text for conflicting or stale contracts. +- Verify external assumptions such as required paths, setup commands, integrations, and runtime capabilities against the current repository before adding a local fallback. +- Structural validation is not semantic alignment. Compare the final wrapper and core responsibilities explicitly, then search for removed owners, stale workflow terms, and conflicting output rules. +- Measure token change with the same method before and after cleanup, but preserve a working trigger and required local safeguards even when they cost a small number of tokens. + ## Token discipline - Keep `SKILL.md` lean and move deeper material into `references/` or reusable tools only when justified. diff --git a/.github/skills/local-agent-sync-external-resources/patches/grill-me.patch b/.github/skills/local-agent-sync-external-resources/patches/grill-me.patch index 5e9f5d25..ecb41998 100644 --- a/.github/skills/local-agent-sync-external-resources/patches/grill-me.patch +++ b/.github/skills/local-agent-sync-external-resources/patches/grill-me.patch @@ -1,15 +1,16 @@ diff --git a/.github/skills/grill-me/SKILL.md b/.github/skills/grill-me/SKILL.md -index 9470cfc..ff05fa6 100644 +index 4b857d0..ff05fa6 100644 --- a/.github/skills/grill-me/SKILL.md +++ b/.github/skills/grill-me/SKILL.md @@ -1,7 +1,37 @@ --- name: grill-me - description: A relentless interview to sharpen a plan or design. +-description: A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go. -disable-model-invocation: true ++description: A relentless interview to sharpen a plan or design. --- --Run a `/grilling` session. +-Run a `/grilling` session, using the `/domain-modeling` skill. +# Grill Me + +## Referenced skills diff --git a/.github/skills/local-agent-sync-external-resources/patches/openai-docx-render-docx.patch b/.github/skills/local-agent-sync-external-resources/patches/openai-docx-render-docx.patch index 69a41de6..d5838cee 100644 --- a/.github/skills/local-agent-sync-external-resources/patches/openai-docx-render-docx.patch +++ b/.github/skills/local-agent-sync-external-resources/patches/openai-docx-render-docx.patch @@ -1,4 +1,5 @@ diff --git a/.github/skills/openai-docx/scripts/render_docx.py b/.github/skills/openai-docx/scripts/render_docx.py +index 907ec89..be4d0d3 100644 --- a/.github/skills/openai-docx/scripts/render_docx.py +++ b/.github/skills/openai-docx/scripts/render_docx.py @@ -1,3 +1,4 @@ diff --git a/.github/skills/local-agent-sync-external-resources/patches/openai-spreadsheet.patch b/.github/skills/local-agent-sync-external-resources/patches/openai-spreadsheet.patch index 49bda19f..3aea6128 100644 --- a/.github/skills/local-agent-sync-external-resources/patches/openai-spreadsheet.patch +++ b/.github/skills/local-agent-sync-external-resources/patches/openai-spreadsheet.patch @@ -1,10 +1,18 @@ diff --git a/.github/skills/openai-spreadsheet/SKILL.md b/.github/skills/openai-spreadsheet/SKILL.md +index 4f41e8d..2236908 100644 --- a/.github/skills/openai-spreadsheet/SKILL.md +++ b/.github/skills/openai-spreadsheet/SKILL.md +@@ -1,5 +1,5 @@ + --- +-name: openai-spreadsheet ++name: "openai-spreadsheet" + description: "Use when tasks involve creating, editing, analyzing, or formatting spreadsheets (`.xlsx`, `.csv`, `.tsv`) with formula-aware workflows, cached recalculation, and visual review." + --- + @@ -33,6 +33,19 @@ IMPORTANT: System and user instructions always take precedence. - Use `openpyxl.chart` for native Excel charts when needed. - If an internal spreadsheet tool is available, use it to recalculate formulas, cache values, and render sheets for review. - + +## Structured Data Evidence Budget +- For large `.xlsx`, `.csv`, and `.tsv` work, keep user-facing evidence compact: + report schema or headers, row counts, column counts, targeted anomalies, diff --git a/.github/skills/local-agent-sync-external-resources/patches/superpowers-brainstorming.patch b/.github/skills/local-agent-sync-external-resources/patches/superpowers-brainstorming.patch index 4d510c30..29768f88 100644 --- a/.github/skills/local-agent-sync-external-resources/patches/superpowers-brainstorming.patch +++ b/.github/skills/local-agent-sync-external-resources/patches/superpowers-brainstorming.patch @@ -1,5 +1,5 @@ diff --git a/.github/skills/superpowers-brainstorming/SKILL.md b/.github/skills/superpowers-brainstorming/SKILL.md -index 16ebd28..47afc6e 100644 +index ddfb2c4..47afc6e 100644 --- a/.github/skills/superpowers-brainstorming/SKILL.md +++ b/.github/skills/superpowers-brainstorming/SKILL.md @@ -7,7 +7,7 @@ description: "You MUST use this before any creative work - creating features, bu @@ -19,7 +19,7 @@ index 16ebd28..47afc6e 100644 +3. **Ask clarifying questions** — use numbered bulk question blocks with `Question`, `Recommendation`, `Why`, and `Default if accepted` 4. **Propose 2-3 approaches** — with trade-offs and your recommendation 5. **Present design** — in sections scaled to their complexity, get user approval after each section --6. **Write design doc** — if the user wants a retained file, save it to `tmp/superpowers/specs/YYYY-MM-DD--design.md`; never commit files from `tmp/` +-6. **Write design doc** — save to `tmp/superpowers/specs/YYYY-MM-DD--design.md` and commit -7. **Spec self-review** — quick inline check for placeholders, contradictions, ambiguity, scope (see below) -8. **User reviews written spec** — ask user to review the spec file before proceeding -9. **Transition to implementation** — invoke writing-plans skill to create implementation plan @@ -108,16 +108,28 @@ index 16ebd28..47afc6e 100644 **Design for isolation and clarity:** - Break the system into smaller units that each have one clear purpose, communicate through well-defined interfaces, and can be understood and tested independently -@@ -104,7 +129,7 @@ digraph brainstorming { +@@ -103,10 +128,11 @@ digraph brainstorming { + **Documentation:** - - Write the validated design (spec) to chat by default --- Persist it to `tmp/superpowers/specs/YYYY-MM-DD--design.md` only when the user explicitly wants a retained file +-- Write the validated design (spec) to `tmp/superpowers/specs/YYYY-MM-DD--design.md` ++- Write the validated design (spec) to chat by default +- Persist it to `tmp/superpowers/specs/YYYY-MM-DD--design.md` only when `Decision: spec first` is chosen and the user explicitly wants a retained file - (User preferences for spec location override this default) - Use elements-of-style:writing-clearly-and-concisely skill if available - - Never commit files from `tmp/` -@@ -128,12 +153,12 @@ Wait for the user's response. If they request changes, make them and re-run the +-- Commit the design document to git ++- Never commit files from `tmp/` + + **Spec Self-Review:** + After writing the spec document, look at it with fresh eyes: +@@ -121,18 +147,18 @@ Fix any issues inline. No need to re-review — just fix and move on. + **User Review Gate:** + After the spec review loop passes, ask the user to review the written spec before proceeding: + +-> "Spec written and committed to ``. Please review it and let me know if you want to make any changes before we start writing out the implementation plan." ++> "Spec written to ``. Please review it and let me know if you want to make any changes before we start writing out the implementation plan." + + Wait for the user's response. If they request changes, make them and re-run the spec review loop. Only proceed once the user approves. **Implementation:** diff --git a/.github/skills/local-agent-sync-external-resources/references/managed-resources.yaml b/.github/skills/local-agent-sync-external-resources/references/managed-resources.yaml index a3b58fa6..606cc7e2 100644 --- a/.github/skills/local-agent-sync-external-resources/references/managed-resources.yaml +++ b/.github/skills/local-agent-sync-external-resources/references/managed-resources.yaml @@ -91,7 +91,7 @@ sources: - upstream: skills/engineering/code-review local: .github/skills/mattpocock-code-review canonical_name: mattpocock-code-review - - upstream: skills/engineering/grill-me + - upstream: skills/engineering/grill-with-docs local: .github/skills/grill-me canonical_name: grill-me - upstream: skills/engineering/research @@ -117,7 +117,7 @@ sources: - upstream: skills/.curated/gh-fix-ci local: .github/skills/openai-gh-fix-ci canonical_name: openai-gh-fix-ci - - upstream: skills/.curated/skill-creator + - upstream: skills/.system/skill-creator local: .github/skills/openai-skill-creator canonical_name: openai-skill-creator - upstream: skills/.curated/pdf diff --git a/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py b/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py index 9a798565..2af57b61 100644 --- a/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py +++ b/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py @@ -61,6 +61,7 @@ def normalize(text: str) -> str: "reference other skills by skill name and behavior only", "prefer bundle-relative references to files under", "do not copy the same material back into `skill.md`", + "compare the wrapper against its core before editing", ] duplicates = [ @@ -71,3 +72,24 @@ def normalize(text: str) -> str: assert not duplicates, ( f"SKILL.md restates {len(duplicates)} phrases also in checklist: {duplicates[:3]}" ) + + +def test_core_backed_wrapper_guidance_is_generic_and_reference_owned() -> None: + skill_text = SKILL_PATH.read_text() + checklist_text = CHECKLIST_PATH.read_text() + + required_guidance = ( + "## Core-backed wrappers", + "Compare the wrapper against its core before editing", + "trigger, repository-local policy, and proven environment fallbacks", + "Do not restate the core's workflow, decision logic, output contract, or validation procedure", + "Structural validation is not semantic alignment", + "paired agent", + ) + + for phrase in required_guidance: + assert phrase in checklist_text + + assert "Compare the wrapper against its core before editing" not in skill_text + assert "internal-review-code" not in checklist_text + assert "mattpocock-code-review" not in checklist_text From c3f1dbae5b618e81527ccb638b43aebae4ba3350 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 13:46:01 +0200 Subject: [PATCH 40/59] refactor: enhance internal gateway writing plans to prevent direct commit instructions and clarify validation requirements --- .../internal-gateway-writing-plans/SKILL.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/skills/internal-gateway-writing-plans/SKILL.md b/.github/skills/internal-gateway-writing-plans/SKILL.md index 6df7dfdd..37a97921 100644 --- a/.github/skills/internal-gateway-writing-plans/SKILL.md +++ b/.github/skills/internal-gateway-writing-plans/SKILL.md @@ -29,8 +29,11 @@ stops after the delegated outcome. 2. Load `superpowers-writing-plans` and let it create a plan, ask a blocking clarification, redirect, or stop with a reason. Pass an explicit anti-scope and the relevant owners already identified in the preflight so the delegated - plan avoids duplicate or speculative tasks at the source. If the delegated - writing outcome persists a retained artifact, require timestamped local + plan avoids duplicate or speculative tasks at the source. Pass an explicit + delivery rule: the delegated plan must not tell the user to run `git add`, + `git commit`, or `git push`, and must not present committing changes as the + default next step unless the user explicitly asks for commit help. If the + delegated writing outcome persists a retained artifact, require timestamped local naming with four-digit 24-hour time: plans use `tmp/superpowers/plans/YYYY-MM-DD-HHMM-.md` and specs use `tmp/superpowers/specs/YYYY-MM-DD-HHMM--design.md` such as @@ -39,8 +42,8 @@ stops after the delegated outcome. Authoring Discipline: ordered tasks, concrete file targets, clear edit intent, validation commands or explicit gaps, stop conditions, and handoff readiness. Reject the draft if any task duplicates an existing owner, adds - speculative scope, or lacks validation commands or an explicit validation - gap. + speculative scope, includes direct commit instructions without explicit + user approval, or lacks validation commands or an explicit validation gap. 4. Stop after the writing outcome and wait for the user's next choice. Preserve handoff quality with targeted rereads only when the delegation has a @@ -54,8 +57,9 @@ real evidence gap. - Single responsibility: each task must carry one clear deliverable tied to the approved target; split or merge tasks that don't. - Fail-fast and redirect: if the delegated plan adds speculative scope, - duplicates an existing owner, or lacks validation commands or an explicit - validation gap, send it back for revision instead of accepting it. + duplicates an existing owner, includes direct `git add`/`git commit`/`git push` + instructions without explicit user approval, or lacks validation commands or + an explicit validation gap, send it back for revision instead of accepting it. DRY, YAGNI, and TDD stay owned by `superpowers-writing-plans`; this section adds only the owner-awareness and redirect gate that the delegated skill does @@ -64,5 +68,5 @@ not enforce. ## Validation - `python3 ./.github/scripts/validate_internal_skills.py --skill internal-gateway-writing-plans --strict` -- Confirm the delegated plan carries ordered tasks, concrete file targets, clear edit intent, validation commands or explicit gaps, and no duplicate-owner or speculative-scope drift. +- Confirm the delegated plan carries ordered tasks, concrete file targets, clear edit intent, validation commands or explicit gaps, no duplicate-owner or speculative-scope drift, and no direct commit instructions unless the user explicitly asked for commit help. - `git diff --check` From bf4494d4e2ba412a737fb3457e535bd9b8fd2bdc Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 13:54:18 +0200 Subject: [PATCH 41/59] refactor: replace mattpocock-code-review with addyosmani-code-review-and-quality across inventory and skill references --- .github/INVENTORY.md | 1 - .github/skills/internal-review-code/SKILL.md | 4 +- .../internal-review-code/agents/openai.yaml | 2 +- .../references/managed-resources.yaml | 17 +++- .../skills/mattpocock-code-review/SKILL.md | 89 ------------------- .../test_internal_skill_creator_contract.py | 2 +- .../scripts/test_cli.py | 2 +- .../scripts/test_manifest.py | 4 +- 8 files changed, 21 insertions(+), 100 deletions(-) delete mode 100644 .github/skills/mattpocock-code-review/SKILL.md diff --git a/.github/INVENTORY.md b/.github/INVENTORY.md index 64f7c8dd..6d9ca5c6 100644 --- a/.github/INVENTORY.md +++ b/.github/INVENTORY.md @@ -123,7 +123,6 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/local-agent-sync-global-copilot-configs-into-repo/SKILL.md` - `.github/skills/local-agent-sync-install-ai-resources/SKILL.md` - `.github/skills/local-copilot-log-analyzer/SKILL.md` -- `.github/skills/mattpocock-code-review/SKILL.md` - `.github/skills/openai-docx/SKILL.md` - `.github/skills/openai-gh-address-comments/SKILL.md` - `.github/skills/openai-gh-fix-ci/SKILL.md` diff --git a/.github/skills/internal-review-code/SKILL.md b/.github/skills/internal-review-code/SKILL.md index ad12b978..5d12ee14 100644 --- a/.github/skills/internal-review-code/SKILL.md +++ b/.github/skills/internal-review-code/SKILL.md @@ -7,7 +7,7 @@ description: Use when reviewing a branch, pull request, work-in-progress diff, o ## Referenced skills -- `mattpocock-code-review`: operating core for the fixed-point Standards and Spec review. +- `addyosmani-code-review-and-quality`: operating core for the fixed-point Standards and Spec review. ## When to use @@ -16,7 +16,7 @@ be reviewed against repository standards and its originating spec. ## Core contract -Use `mattpocock-code-review` as the complete review engine and follow its +Use `addyosmani-code-review-and-quality` as the complete review engine and follow its process end to end. This wrapper provides the stable repository-owned entrypoint and the local context below; it does not redefine the core's axes, process, or output. diff --git a/.github/skills/internal-review-code/agents/openai.yaml b/.github/skills/internal-review-code/agents/openai.yaml index daa9fb33..b3d87167 100644 --- a/.github/skills/internal-review-code/agents/openai.yaml +++ b/.github/skills/internal-review-code/agents/openai.yaml @@ -1,4 +1,4 @@ interface: display_name: "Internal Review Code" short_description: "Repository wrapper for two-axis code review" - default_prompt: "Use $internal-review-code as the repository wrapper and follow $mattpocock-code-review as its review core." + default_prompt: "Use $internal-review-code as the repository wrapper and follow $addyosmani-code-review-and-quality as its review core." diff --git a/.github/skills/local-agent-sync-external-resources/references/managed-resources.yaml b/.github/skills/local-agent-sync-external-resources/references/managed-resources.yaml index 606cc7e2..e11ffe1b 100644 --- a/.github/skills/local-agent-sync-external-resources/references/managed-resources.yaml +++ b/.github/skills/local-agent-sync-external-resources/references/managed-resources.yaml @@ -31,6 +31,9 @@ sources: - upstream: skills/secret-scanning local: .github/skills/awesome-copilot-secret-scanning canonical_name: awesome-copilot-secret-scanning + - upstream: skills/security-review + local: .github/skills/awesome-copilot-security-review + canonical_name: awesome-copilot-security-review obra-superpowers: repository: https://github.com/obra/superpowers.git ref: d884ae04edebef577e82ff7c4e143debd0bbec99 @@ -88,9 +91,6 @@ sources: repository: https://github.com/mattpocock/skills.git ref: efa058a349f5ce98b6115bf8b4e0d0ef9c310e0d assets: - - upstream: skills/engineering/code-review - local: .github/skills/mattpocock-code-review - canonical_name: mattpocock-code-review - upstream: skills/engineering/grill-with-docs local: .github/skills/grill-me canonical_name: grill-me @@ -165,6 +165,13 @@ sources: - upstream: skills/network-engineer local: .github/skills/antigravity-network-engineer canonical_name: antigravity-network-engineer + addyosmani-agent-skills: + repository: https://github.com/addyosmani/agent-skills.git + ref: 0e63d8ea962ecac51637421d8fff63702fac766b + assets: + - upstream: skills/code-review-and-quality + local: .github/skills/addyosmani-code-review-and-quality + canonical_name: addyosmani-code-review-and-quality normalizations: - source: obra-superpowers from: docs/superpowers @@ -214,6 +221,10 @@ watchlist: upstream_id: caveman local_owner: retired reason: The previously retained caveman import was retired from the managed catalog by explicit user scope reduction. Do not reimport unless the user re-approves the retained import. + - source_family: mattpocock/skills + upstream_id: code-review + local_owner: retired + reason: The mattpocock-code-review import was replaced by addyosmani-code-review-and-quality from addyosmani/agent-skills. Do not reimport unless the user re-approves the retained import. - source_family: addyosmani/agent-skills upstream_id: idea-refine local_owner: internal-gateway-idea diff --git a/.github/skills/mattpocock-code-review/SKILL.md b/.github/skills/mattpocock-code-review/SKILL.md deleted file mode 100644 index f8f16107..00000000 --- a/.github/skills/mattpocock-code-review/SKILL.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -name: mattpocock-code-review -description: Review the changes since a fixed point (commit, branch, tag, or merge-base) along two axes — Standards (does the code follow this repo's documented coding standards?) and Spec (does the code match what the originating issue/PRD asked for?). Runs both reviews in parallel sub-agents and reports them side by side. Use when the user wants to review a branch, a PR, work-in-progress changes, or asks to "review since X". ---- - -Two-axis review of the diff between `HEAD` and a fixed point the user supplies: - -- **Standards** — does the code conform to this repo's documented coding standards? -- **Spec** — does the code faithfully implement the originating issue / PRD / spec? - -Both axes run as **parallel sub-agents** so they don't pollute each other's context, then this skill aggregates their findings. - -The issue tracker should have been provided to you — run `/setup-matt-pocock-skills` if `docs/agents/issue-tracker.md` is missing. - -## Process - -### 1. Pin the fixed point - -Whatever the user said is the fixed point — a commit SHA, branch name, tag, `main`, `HEAD~5`, etc. If they didn't specify one, ask for it. - -Capture the diff command once: `git diff ...HEAD` (three-dot, so the comparison is against the merge-base). Also note the list of commits via `git log ..HEAD --oneline`. - -Before going further, confirm the fixed point resolves (`git rev-parse `) and the diff is non-empty. A bad ref or empty diff should fail here — not inside two parallel sub-agents. - -### 2. Identify the spec source - -Look for the originating spec, in this order: - -1. Issue references in the commit messages (`#123`, `Closes #45`, GitLab `!67`, etc.) — fetch via the workflow in `docs/agents/issue-tracker.md`. -2. A path the user passed as an argument. -3. A PRD/spec file under `docs/`, `specs/`, or `.scratch/` matching the branch name or feature. -4. If nothing is found, ask the user where the spec is. If they say there isn't one, the **Spec** sub-agent will skip and report "no spec available". - -### 3. Identify the standards sources - -Anything in the repo that documents how code should be written, such as `CODING_STANDARDS.md` or `CONTRIBUTING.md`. - -On top of whatever the repo documents, the Standards axis always carries the **smell baseline** below — a fixed set of Fowler code smells (_Refactoring_, ch.3) that applies even when a repo documents nothing. Two rules bind it: - -- **The repo overrides.** A documented repo standard always wins; where it endorses something the baseline would flag, suppress the smell. -- **Always a judgement call.** Each smell is a labelled heuristic ("possible Feature Envy"), never a hard violation — and, like any standard here, skip anything tooling already enforces. - -Each smell reads *what it is* → *how to fix*; match it against the diff: - -- **Mysterious Name** — a function, variable, or type whose name doesn't reveal what it does or holds. → rename it; if no honest name comes, the design's murky. -- **Duplicated Code** — the same logic shape appears in more than one hunk or file in the change. → extract the shared shape, call it from both. -- **Feature Envy** — a method that reaches into another object's data more than its own. → move the method onto the data it envies. -- **Data Clumps** — the same few fields or params keep travelling together (a type wanting to be born). → bundle them into one type, pass that. -- **Primitive Obsession** — a primitive or string standing in for a domain concept that deserves its own type. → give the concept its own small type. -- **Repeated Switches** — the same `switch`/`if`-cascade on the same type recurs across the change. → replace with polymorphism, or one map both sites share. -- **Shotgun Surgery** — one logical change forces scattered edits across many files in the diff. → gather what changes together into one module. -- **Divergent Change** — one file or module is edited for several unrelated reasons. → split so each module changes for one reason. -- **Speculative Generality** — abstraction, parameters, or hooks added for needs the spec doesn't have. → delete it; inline back until a real need shows. -- **Message Chains** — long `a.b().c().d()` navigation the caller shouldn't depend on. → hide the walk behind one method on the first object. -- **Middle Man** — a class or function that mostly just delegates onward. → cut it, call the real target direct. -- **Refused Bequest** — a subclass or implementer that ignores or overrides most of what it inherits. → drop the inheritance, use composition. - -### 4. Spawn both sub-agents in parallel - -Send a single message with two `Agent` tool calls. Use the `general-purpose` subagent for both. - -**Standards sub-agent prompt** — include: - -- The full diff command and commit list. -- The list of standards-source files you found in step 3, **plus the smell baseline from step 3** pasted in full — the sub-agent has no other access to it. -- The brief: "Report — per file/hunk where relevant — (a) every place the diff violates a documented standard: cite the standard (file + the rule); and (b) any baseline smell you spot: name it and quote the hunk. Distinguish hard violations from judgement calls — documented-standard breaches can be hard, but baseline smells are always judgement calls, and a documented repo standard overrides the baseline. Skip anything tooling enforces. Under 400 words." - -**Spec sub-agent prompt** — include: - -- The diff command and commit list. -- The path or fetched contents of the spec. -- The brief: "Report: (a) requirements the spec asked for that are missing or partial; (b) behaviour in the diff that wasn't asked for (scope creep); (c) requirements that look implemented but where the implementation looks wrong. Quote the spec line for each finding. Under 400 words." - -If the spec is missing, skip the Spec sub-agent and note this in the final report. - -### 5. Aggregate - -Present the two reports under `## Standards` and `## Spec` headings, verbatim or lightly cleaned. Do **not** merge or rerank findings — the two axes are deliberately separate (see _Why two axes_). - -End with a one-line summary: total findings per axis, and the worst issue _within each axis_ (if any). Don't pick a single winner across axes — that's the reranking the separation exists to prevent. - -## Why two axes - -A change can pass one axis and fail the other: - -- Code that follows every standard but implements the wrong thing → **Standards pass, Spec fail.** -- Code that does exactly what the issue asked but breaks the project's conventions → **Spec pass, Standards fail.** - -Reporting them separately stops one axis from masking the other. diff --git a/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py b/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py index 2af57b61..0d30db1b 100644 --- a/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py +++ b/tests/github/skills/internal-skill-creator/test_internal_skill_creator_contract.py @@ -92,4 +92,4 @@ def test_core_backed_wrapper_guidance_is_generic_and_reference_owned() -> None: assert "Compare the wrapper against its core before editing" not in skill_text assert "internal-review-code" not in checklist_text - assert "mattpocock-code-review" not in checklist_text + assert "addyosmani-code-review-and-quality" not in checklist_text diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_cli.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_cli.py index f25ba504..56541c27 100644 --- a/tests/github/skills/local-agent-sync-external-resources/scripts/test_cli.py +++ b/tests/github/skills/local-agent-sync-external-resources/scripts/test_cli.py @@ -61,7 +61,7 @@ def test_audit_does_not_fetch_or_write(repo_root: Path) -> None: payload = json.loads(result.stdout) assert payload["mode"] == "audit" assert payload["repository_changed"] is False - assert payload["managed_assets"] == 43 + assert payload["managed_assets"] == 44 def test_apply_refuses_dirty_target(tmp_path: Path) -> None: diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_manifest.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_manifest.py index 9371a713..ac14c34d 100644 --- a/tests/github/skills/local-agent-sync-external-resources/scripts/test_manifest.py +++ b/tests/github/skills/local-agent-sync-external-resources/scripts/test_manifest.py @@ -28,8 +28,8 @@ def test_live_manifest_preserves_declared_scope(repo_root: Path) -> None: / ".github/skills/local-agent-sync-external-resources/references/managed-resources.yaml" ) - assert len(manifest.assets) == 43 - assert len(manifest.watchlist) == 12 + assert len(manifest.assets) == 44 + assert len(manifest.watchlist) == 13 assert { item.local for item in manifest.assets if item.source == "obra-superpowers" } == { From f9366f1c6d1ed143c5cfa015840c89de177b7308 Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 14:32:23 +0200 Subject: [PATCH 42/59] refactor: enhance sync_external_resources and core functionality with improved patch handling and validation --- .../scripts/sync_external_resources.py | 151 ++++++++++-- .../scripts/sync_external_resources_core.py | 49 +++- .../scripts/test_candidate.py | 192 +++++++++++++++ .../scripts/test_cli.py | 232 ++++++++++++++++++ .../scripts/test_manifest.py | 38 +++ 5 files changed, 638 insertions(+), 24 deletions(-) diff --git a/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources.py b/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources.py index 4d921cb0..b7cdf7ea 100644 --- a/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources.py +++ b/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources.py @@ -2,9 +2,12 @@ from __future__ import annotations import argparse +import hashlib import json +import shutil import subprocess import sys +import tempfile from dataclasses import dataclass, field from pathlib import Path from typing import Literal, Sequence @@ -13,6 +16,7 @@ sys.path.insert(0, SCRIPT_DIR.as_posix()) from sync_external_resources_core import ( # noqa: E402 + ManagedAsset, ManagedResources, OverrideResult, SyncCommandError, @@ -23,6 +27,7 @@ normalize_candidate, replay_overrides, validate_external_workspace, + validate_override_patches, ) DEFAULT_MANIFEST = ( @@ -96,20 +101,99 @@ def _run_git(repo_root: Path, args: list[str]) -> subprocess.CompletedProcess[st return result +def _materialize_managed_repo_snapshot( + repo_root: Path, resources: ManagedResources, snapshot: Path +) -> None: + snapshot.mkdir(parents=True, exist_ok=True) + for asset in resources.assets: + src = repo_root / asset.local + dst = snapshot / asset.local + if not src.exists(): + continue + dst.parent.mkdir(parents=True, exist_ok=True) + if src.is_dir(): + shutil.copytree(src, dst, dirs_exist_ok=True) + else: + shutil.copy2(src, dst) + + def _build_candidate_patch( repo_root: Path, candidate: Path, resources: ManagedResources ) -> str: - diff_result = subprocess.run( - ["git", "diff", "--no-index", "--", "."], - cwd=candidate, - check=False, - capture_output=True, - text=True, + with tempfile.TemporaryDirectory(prefix="sync-snapshot-") as raw_snapshot: + snapshot = Path(raw_snapshot) / "snapshot" + _materialize_managed_repo_snapshot(repo_root, resources, snapshot) + diff_result = subprocess.run( + [ + "git", + "diff", + "--no-index", + "--", + str(snapshot), + str(candidate), + ], + cwd=repo_root, + check=False, + capture_output=True, + text=True, + ) + return _rewrite_patch_paths( + diff_result.stdout, str(snapshot), str(candidate) ) - return diff_result.stdout -def _apply_candidate_patch(repo_root: Path, patch_text: str) -> bool: +def _rewrite_patch_paths( + patch_text: str, snapshot_prefix: str, candidate_prefix: str +) -> str: + lines = patch_text.splitlines(keepends=True) + result: list[str] = [] + for line in lines: + if line.startswith("--- "): + path = line[4:].strip() + if path.startswith("a" + snapshot_prefix): + relative = path[len("a" + snapshot_prefix) :].lstrip("/") + result.append(f"--- a/{relative}\n") + else: + result.append(line) + elif line.startswith("+++ "): + path = line[4:].strip() + if path.startswith("b" + candidate_prefix): + relative = path[len("b" + candidate_prefix) :].lstrip("/") + result.append(f"+++ b/{relative}\n") + else: + result.append(line) + else: + result.append(line) + return "".join(result) + + +def _sha256_file(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as fh: + for chunk in iter(lambda: fh.read(65536), b""): + digest.update(chunk) + return digest.hexdigest() + + +def _snapshot_managed_targets( + repo_root: Path, resources: ManagedResources +) -> dict[str, str]: + snapshot: dict[str, str] = {} + for asset in resources.assets: + target = repo_root / asset.local + if target.is_file(): + snapshot[asset.local] = _sha256_file(target) + elif target.is_dir(): + for file_path in sorted(target.rglob("*")): + if file_path.is_file(): + rel = file_path.relative_to(repo_root).as_posix() + snapshot[rel] = _sha256_file(file_path) + return snapshot + + +def _apply_candidate_patch( + repo_root: Path, patch_text: str, resources: ManagedResources +) -> bool: if not patch_text.strip(): return False result = subprocess.run( @@ -122,6 +206,7 @@ def _apply_candidate_patch(repo_root: Path, patch_text: str) -> bool: ) if result.returncode != 0: raise ValueError(f"Patch check failed: {result.stderr[:500]}") + before = _snapshot_managed_targets(repo_root, resources) subprocess.run( ["git", "apply", "-"], cwd=repo_root, @@ -130,7 +215,12 @@ def _apply_candidate_patch(repo_root: Path, patch_text: str) -> bool: capture_output=True, text=True, ) - return True + after = _snapshot_managed_targets(repo_root, resources) + if before != after: + return True + raise ValueError( + "Patch applied but no managed target files changed in the repository." + ) def _audit( @@ -143,7 +233,18 @@ def _audit( if dirty: blockers.append(f"dirty managed targets: {', '.join(dirty)}") - overrides = load_overrides(overrides_path) if overrides_path.exists() else () + validations: list[str] = ["manifest-parsed"] + if overrides_path.exists(): + overrides = load_overrides(overrides_path) + validations.append("overrides-parsed") + bundle_root = overrides_path.parent.parent + try: + validate_override_patches(overrides, bundle_root) + validations.append("override-patches-exist") + except ValueError as exc: + blockers.append(str(exc)) + else: + overrides = () return SyncOutcome( mode="audit", @@ -151,12 +252,16 @@ def _audit( managed_assets=len(resources.assets), changed_paths=(), override_results=(), - validations=("manifest-parsed", "overrides-parsed"), + validations=tuple(validations), blockers=tuple(blockers), repository_changed=False, ) +def _resolve_sources_root(workspace: Path, source_root: Path | None) -> Path: + return source_root if source_root is not None else workspace / "sources" + + def _plan( repo_root: Path, workspace: Path, @@ -166,12 +271,17 @@ def _plan( ) -> SyncOutcome: validate_external_workspace(repo_root, workspace) + sources_root = _resolve_sources_root(workspace, source_root) candidate = workspace / "candidate" - materialize_candidate(resources, workspace, candidate) + materialize_candidate(resources, workspace, candidate, sources_root=sources_root) changed = normalize_candidate(resources, candidate) - overrides = load_overrides(overrides_path) if overrides_path.exists() else () - override_results = replay_overrides(candidate, overrides, overrides_path.parent.parent) if overrides else () + override_results: tuple[OverrideResult, ...] = () + if overrides_path.exists(): + overrides = load_overrides(overrides_path) + bundle_root = overrides_path.parent.parent + validate_override_patches(overrides, bundle_root) + override_results = replay_overrides(candidate, overrides, bundle_root) return SyncOutcome( mode="plan", @@ -208,15 +318,20 @@ def _apply( repository_changed=False, ) + sources_root = _resolve_sources_root(workspace, source_root) candidate = workspace / "candidate" - materialize_candidate(resources, workspace, candidate) + materialize_candidate(resources, workspace, candidate, sources_root=sources_root) changed = normalize_candidate(resources, candidate) - overrides = load_overrides(overrides_path) if overrides_path.exists() else () - override_results = replay_overrides(candidate, overrides, overrides_path.parent.parent) if overrides else () + override_results: tuple[OverrideResult, ...] = () + if overrides_path.exists(): + overrides = load_overrides(overrides_path) + bundle_root = overrides_path.parent.parent + validate_override_patches(overrides, bundle_root) + override_results = replay_overrides(candidate, overrides, bundle_root) patch_text = _build_candidate_patch(repo_root, candidate, resources) - repository_changed = _apply_candidate_patch(repo_root, patch_text) + repository_changed = _apply_candidate_patch(repo_root, patch_text, resources) return SyncOutcome( mode="apply", diff --git a/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py b/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py index 271cc0cb..d7e8f858 100644 --- a/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py +++ b/.github/skills/local-agent-sync-external-resources/scripts/sync_external_resources_core.py @@ -255,19 +255,23 @@ def materialize_candidate( resources: ManagedResources, workspace: Path, candidate: Path, + sources_root: Path | None = None, ) -> None: if candidate.exists(): shutil.rmtree(candidate) candidate.mkdir(parents=True) + if sources_root is None: + sources_root = workspace / "sources" + + missing: list[tuple[str, str]] = [] for source in resources.sources: - source_root = workspace / "sources" / source.source_id + source_dir = sources_root / source.source_id for asset in source.assets: - upstream_path = source_root / asset.upstream + upstream_path = source_dir / asset.upstream if not upstream_path.exists(): - raise ValueError( - f"Missing upstream path: {asset.upstream} in source {source.source_id}" - ) + missing.append((source.source_id, asset.upstream)) + continue target_path = candidate / asset.local target_path.parent.mkdir(parents=True, exist_ok=True) if upstream_path.is_dir(): @@ -275,6 +279,10 @@ def materialize_candidate( else: shutil.copy2(upstream_path, target_path) + if missing: + details = "; ".join(f"{sid}:{path}" for sid, path in missing) + raise ValueError(f"Missing upstream paths: {details}") + _FRONTMATTER_NAME_RE = re.compile(r"^(name\s*:\s*).*$", re.MULTILINE) @@ -368,6 +376,23 @@ def load_overrides(path: Path) -> tuple[ImportedOverride, ...]: return tuple(overrides) +def _resolve_override_patch(bundle_root: Path, patch_path: str) -> Path: + return bundle_root / patch_path + + +def validate_override_patches( + overrides: tuple[ImportedOverride, ...], + bundle_root: Path, +) -> None: + missing: list[str] = [] + for override in overrides: + resolved = _resolve_override_patch(bundle_root, override.patch_path) + if not resolved.exists(): + missing.append(override.patch_path) + if missing: + raise ValueError(f"Override patch missing: {', '.join(missing)}") + + def select_overrides( overrides: tuple[ImportedOverride, ...], requested_ids: tuple[str, ...], @@ -405,12 +430,23 @@ def verify_override_hash( ) +def _init_trial_git_repo(trial: Path) -> None: + _run_command(["git", "init"], cwd=trial) + _run_command(["git", "config", "user.email", "test@test.com"], cwd=trial) + _run_command(["git", "config", "user.name", "Test"], cwd=trial) + _run_command(["git", "add", "-A"], cwd=trial) + _run_command( + ["git", "commit", "-m", "trial-baseline", "--allow-empty"], + cwd=trial, + ) + + def _replay_one_override( trial_repo: Path, override: ImportedOverride, patches_root: Path, ) -> OverrideResult: - patch_file = patches_root / override.patch_path + patch_file = _resolve_override_patch(patches_root, override.patch_path) if not patch_file.exists(): raise ValueError(f"Override patch missing: {override.patch_path}") @@ -493,6 +529,7 @@ def replay_overrides( with tempfile.TemporaryDirectory(prefix="external-override-") as raw_trial: trial = Path(raw_trial) / "candidate" shutil.copytree(candidate_repo, trial) + _init_trial_git_repo(trial) results: list[OverrideResult] = [] for item in overrides: result = _replay_one_override(trial, item, patches_root) diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_candidate.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_candidate.py index 5be6d3a8..b474e06a 100644 --- a/tests/github/skills/local-agent-sync-external-resources/scripts/test_candidate.py +++ b/tests/github/skills/local-agent-sync-external-resources/scripts/test_candidate.py @@ -15,6 +15,7 @@ ) sys.path.insert(0, SCRIPT_DIR.as_posix()) +from sync_external_resources import _build_candidate_patch # noqa: E402 from sync_external_resources_core import ( # noqa: E402 ImportedOverride, ManagedAsset, @@ -28,6 +29,7 @@ replay_overrides, select_overrides, validate_external_workspace, + validate_override_patches, verify_override_hash, ) @@ -311,3 +313,193 @@ def test_load_overrides_from_yaml(tmp_path: Path) -> None: assert len(overrides) == 1 assert overrides[0].override_id == "test-override" assert overrides[0].expected_content_hash == "abcdef0123456789" * 4 + + +def test_build_candidate_patch_detects_repo_vs_candidate_diff( + tmp_path: Path, +) -> None: + repo = tmp_path / "repo" + repo.mkdir() + _run_git(repo, ["init"]) + _run_git(repo, ["config", "user.email", "test@test.com"]) + _run_git(repo, ["config", "user.name", "Test"]) + + target = repo / ".github/skills/example/SKILL.md" + target.parent.mkdir(parents=True) + target.write_text("---\nname: example\n---\nOld content.\n", encoding="utf-8") + _commit_all(repo) + + candidate = tmp_path / "candidate" + candidate.mkdir() + cand_target = candidate / ".github/skills/example/SKILL.md" + cand_target.parent.mkdir(parents=True) + cand_target.write_text( + "---\nname: example\n---\nNew content.\n", encoding="utf-8" + ) + + asset = ManagedAsset( + source="test-source", + upstream="skills/example", + local=".github/skills/example", + canonical_name="example", + ) + resources = ManagedResources( + sources=( + ManagedSource( + source_id="test-source", + repository="https://example.com/repo.git", + ref="abc", + assets=(asset,), + ), + ), + replacements=(), + watchlist=(), + ) + + patch = _build_candidate_patch(repo, candidate, resources) + assert patch.strip(), "patch must be non-empty when candidate differs from repo" + assert "New content." in patch + + +def test_materialize_candidate_uses_explicit_source_root( + tmp_path: Path, +) -> None: + workspace = tmp_path / "workspace" + workspace.mkdir() + external_sources = tmp_path / "external-sources" + source_checkout = external_sources / "test-source" / "skills" / "example" + source_checkout.mkdir(parents=True) + (source_checkout / "SKILL.md").write_text( + "---\nname: upstream-name\n---\n", encoding="utf-8" + ) + + asset = ManagedAsset( + source="test-source", + upstream="skills/example", + local=".github/skills/example", + canonical_name="example", + ) + source = ManagedSource( + source_id="test-source", + repository="https://example.com/repo.git", + ref="abc", + assets=(asset,), + ) + resources = ManagedResources( + sources=(source,), replacements=(), watchlist=() + ) + + candidate = tmp_path / "candidate" + materialize_candidate(resources, workspace, candidate, sources_root=external_sources) + + target = candidate / ".github/skills/example/SKILL.md" + assert target.exists() + assert "upstream-name" in target.read_text(encoding="utf-8") + + +def test_materialize_candidate_reports_all_missing_upstreams( + tmp_path: Path, +) -> None: + workspace = tmp_path / "workspace" + workspace.mkdir() + sources_root = tmp_path / "sources" + sources_root.mkdir() + + asset_a = ManagedAsset( + source="src-a", + upstream="skills/a", + local=".github/skills/a", + canonical_name="a", + ) + asset_b = ManagedAsset( + source="src-b", + upstream="skills/b", + local=".github/skills/b", + canonical_name="b", + ) + resources = ManagedResources( + sources=( + ManagedSource( + source_id="src-a", + repository="https://example.com/a.git", + ref="abc", + assets=(asset_a,), + ), + ManagedSource( + source_id="src-b", + repository="https://example.com/b.git", + ref="def", + assets=(asset_b,), + ), + ), + replacements=(), + watchlist=(), + ) + + candidate = tmp_path / "candidate" + with pytest.raises(ValueError, match="Missing upstream paths") as exc_info: + materialize_candidate(resources, workspace, candidate, sources_root=sources_root) + message = str(exc_info.value) + assert "src-a" in message + assert "src-b" in message + + +def test_load_overrides_rejects_missing_patch_file(tmp_path: Path) -> None: + registry = tmp_path / "overrides.yaml" + registry.write_text( + """\ +version: 1 +overrides: + - id: test-override + target_path: .github/skills/test/SKILL.md + patch_path: patches/nonexistent.patch + apply_strategy: git-apply + expected_content_hash: abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789 +""", + encoding="utf-8", + ) + + overrides = load_overrides(registry) + bundle_root = tmp_path + with pytest.raises(ValueError, match="Override patch missing"): + validate_override_patches(overrides, bundle_root) + + +def test_override_3way_replay_uses_real_git_repo( + candidate_repo: Path, +) -> None: + target = candidate_repo / ".github/skills/test/SKILL.md" + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text("---\nname: test\n---\nOriginal.\n", encoding="utf-8") + _commit_all(candidate_repo) + + patch_dir = candidate_repo / "patches" + patch_dir.mkdir(exist_ok=True) + patch_path = patch_dir / "test-3way.patch" + + target.write_text("---\nname: test\n---\nPatched.\n", encoding="utf-8") + result = subprocess.run( + ["git", "diff", "--", ".github/skills/test/SKILL.md"], + cwd=candidate_repo, + capture_output=True, + text=True, + check=True, + ) + patch_path.write_text(result.stdout, encoding="utf-8") + target.write_text("---\nname: test\n---\nOriginal.\n", encoding="utf-8") + + override = ImportedOverride( + override_id="test-3way", + target_path=".github/skills/test/SKILL.md", + patch_path=patch_path.relative_to(candidate_repo).as_posix(), + apply_strategy="git-apply-3way", + expected_content_hash=_sha256("---\nname: test\n---\nPatched.\n"), + ) + + results = replay_overrides( + candidate_repo, (override,), patches_root=candidate_repo + ) + + assert len(results) == 1 + assert results[0].status == "applied" + assert "Patched." in target.read_text(encoding="utf-8") diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_cli.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_cli.py index 56541c27..ee32b4f0 100644 --- a/tests/github/skills/local-agent-sync-external-resources/scripts/test_cli.py +++ b/tests/github/skills/local-agent-sync-external-resources/scripts/test_cli.py @@ -131,6 +131,238 @@ def test_apply_refuses_dirty_target(tmp_path: Path) -> None: assert result.returncode == 2 +def test_apply_reports_repository_changed_when_candidate_diff_applies( + tmp_path: Path, +) -> None: + repo = tmp_path / "repo" + repo.mkdir() + _run_git(repo, ["init"]) + _run_git(repo, ["config", "user.email", "test@test.com"]) + _run_git(repo, ["config", "user.name", "Test"]) + + manifest_src = tmp_path / "manifest.yaml" + manifest_src.write_text( + """\ +version: 1 +sources: + test-source: + repository: https://example.com/repo.git + ref: abc123 + assets: + - upstream: skills/example + local: .github/skills/example + canonical_name: example +watchlist: [] +""", + encoding="utf-8", + ) + + overrides_src = tmp_path / "overrides.yaml" + overrides_src.write_text( + """\ +version: 1 +overrides: [] +""", + encoding="utf-8", + ) + + target = repo / ".github/skills/example/SKILL.md" + target.parent.mkdir(parents=True) + target.write_text("---\nname: example\n---\nOld content.\n", encoding="utf-8") + _commit_all(repo) + + workspace = tmp_path / "external-workspace" + workspace.mkdir() + source_dir = workspace / "sources" / "test-source" / "skills" / "example" + source_dir.mkdir(parents=True) + (source_dir / "SKILL.md").write_text( + "---\nname: example\n---\nNew content.\n", encoding="utf-8" + ) + + result = subprocess.run( + [ + sys.executable, + str(SCRIPT_DIR / "sync_external_resources.py"), + "apply", + "--repo-root", + str(repo), + "--workspace", + str(workspace), + "--manifest", + str(manifest_src), + "--overrides", + str(overrides_src), + "--format", + "json", + ], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + assert payload["repository_changed"] is True + assert "New content." in target.read_text(encoding="utf-8") + + +def test_plan_uses_explicit_source_root(tmp_path: Path) -> None: + repo = tmp_path / "repo" + repo.mkdir() + _run_git(repo, ["init"]) + _run_git(repo, ["config", "user.email", "test@test.com"]) + _run_git(repo, ["config", "user.name", "Test"]) + + manifest_src = tmp_path / "manifest.yaml" + manifest_src.write_text( + """\ +version: 1 +sources: + test-source: + repository: https://example.com/repo.git + ref: abc123 + assets: + - upstream: skills/example + local: .github/skills/example + canonical_name: example +watchlist: [] +""", + encoding="utf-8", + ) + + overrides_src = tmp_path / "overrides.yaml" + overrides_src.write_text( + """\ +version: 1 +overrides: [] +""", + encoding="utf-8", + ) + + target = repo / ".github/skills/example/SKILL.md" + target.parent.mkdir(parents=True) + target.write_text("---\nname: example\n---\nOld content.\n", encoding="utf-8") + _commit_all(repo) + + workspace = tmp_path / "external-workspace" + workspace.mkdir() + external_sources = tmp_path / "external-sources" / "test-source" / "skills" / "example" + external_sources.mkdir(parents=True) + (external_sources / "SKILL.md").write_text( + "---\nname: example\n---\nNew content.\n", encoding="utf-8" + ) + + result = subprocess.run( + [ + sys.executable, + str(SCRIPT_DIR / "sync_external_resources.py"), + "plan", + "--repo-root", + str(repo), + "--workspace", + str(workspace), + "--source-root", + str(tmp_path / "external-sources"), + "--manifest", + str(manifest_src), + "--overrides", + str(overrides_src), + "--format", + "json", + ], + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode == 0, result.stderr + payload = json.loads(result.stdout) + assert payload["mode"] == "plan" + + +def test_plan_then_apply_end_to_end(tmp_path: Path) -> None: + repo = tmp_path / "repo" + repo.mkdir() + _run_git(repo, ["init"]) + _run_git(repo, ["config", "user.email", "test@test.com"]) + _run_git(repo, ["config", "user.name", "Test"]) + + manifest_src = tmp_path / "manifest.yaml" + manifest_src.write_text( + """\ +version: 1 +sources: + test-source: + repository: https://example.com/repo.git + ref: abc123 + assets: + - upstream: skills/example + local: .github/skills/example + canonical_name: example +watchlist: [] +""", + encoding="utf-8", + ) + + overrides_src = tmp_path / "overrides.yaml" + overrides_src.write_text( + """\ +version: 1 +overrides: [] +""", + encoding="utf-8", + ) + + target = repo / ".github/skills/example/SKILL.md" + target.parent.mkdir(parents=True) + target.write_text("---\nname: example\n---\nOld content.\n", encoding="utf-8") + _commit_all(repo) + + workspace = tmp_path / "external-workspace" + workspace.mkdir() + source_dir = workspace / "sources" / "test-source" / "skills" / "example" + source_dir.mkdir(parents=True) + (source_dir / "SKILL.md").write_text( + "---\nname: example\n---\nNew content.\n", encoding="utf-8" + ) + + common_args = [ + sys.executable, + str(SCRIPT_DIR / "sync_external_resources.py"), + "--repo-root", + str(repo), + "--workspace", + str(workspace), + "--manifest", + str(manifest_src), + "--overrides", + str(overrides_src), + "--format", + "json", + ] + + plan_result = subprocess.run( + [*common_args, "plan"], + capture_output=True, + text=True, + check=False, + ) + assert plan_result.returncode == 0, plan_result.stderr + plan_payload = json.loads(plan_result.stdout) + assert plan_payload["repository_changed"] is False + + apply_result = subprocess.run( + [*common_args, "apply"], + capture_output=True, + text=True, + check=False, + ) + assert apply_result.returncode == 0, apply_result.stderr + apply_payload = json.loads(apply_result.stdout) + assert apply_payload["repository_changed"] is True + assert "New content." in target.read_text(encoding="utf-8") + + def test_bundle_exposes_one_public_cli(repo_root: Path) -> None: scripts = repo_root / ".github/skills/local-agent-sync-external-resources/scripts" public_scripts = sorted( diff --git a/tests/github/skills/local-agent-sync-external-resources/scripts/test_manifest.py b/tests/github/skills/local-agent-sync-external-resources/scripts/test_manifest.py index ac14c34d..b10f7140 100644 --- a/tests/github/skills/local-agent-sync-external-resources/scripts/test_manifest.py +++ b/tests/github/skills/local-agent-sync-external-resources/scripts/test_manifest.py @@ -14,6 +14,8 @@ from sync_external_resources_core import ( # noqa: E402 load_managed_resources, + load_overrides, + validate_override_patches, ) @@ -72,3 +74,39 @@ def test_manifest_rejects_duplicate_local_paths(tmp_path: Path) -> None: with pytest.raises(ValueError, match="duplicate local path"): load_managed_resources(path) + + +def test_live_override_registry_patch_paths_exist(repo_root: Path) -> None: + overrides_path = ( + repo_root + / ".github/skills/local-agent-sync-external-resources/references/imported-asset-overrides.yaml" + ) + bundle_root = repo_root / ".github/skills/local-agent-sync-external-resources" + + overrides = load_overrides(overrides_path) + validate_override_patches(overrides, bundle_root) + + +def test_live_override_targets_sit_under_managed_assets(repo_root: Path) -> None: + manifest = load_managed_resources( + repo_root + / ".github/skills/local-agent-sync-external-resources/references/managed-resources.yaml" + ) + overrides_path = ( + repo_root + / ".github/skills/local-agent-sync-external-resources/references/imported-asset-overrides.yaml" + ) + + managed_local_paths = {asset.local for asset in manifest.assets} + overrides = load_overrides(overrides_path) + + for override in overrides: + matched = any( + override.target_path == local + or override.target_path.startswith(local + "/") + for local in managed_local_paths + ) + assert matched, ( + f"Override {override.override_id} target {override.target_path} " + f"does not sit under any managed local asset" + ) From 00bea258bc677c1150f866584aa1f6577105888a Mon Sep 17 00:00:00 2001 From: diegoitaliait Date: Thu, 9 Jul 2026 14:40:13 +0200 Subject: [PATCH 43/59] Add detailed vulnerability categories and detection guidance; create vulnerable package watchlist; enhance skills documentation and execution plans --- .github/INVENTORY.md | 5 + .../SKILL.md | 381 ++++++++++++++++++ .../references/workflow-configuration.md | 2 +- .../awesome-copilot-security-review/SKILL.md | 168 ++++++++ .../references/language-patterns.md | 221 ++++++++++ .../references/report-format.md | 194 +++++++++ .../references/secret-patterns.md | 178 ++++++++ .../references/vuln-categories.md | 281 +++++++++++++ .../references/vulnerable-packages.md | 111 +++++ .../internal-gateway-execute-plans/SKILL.md | 21 +- .../scripts/sync_external_resources.py | 56 ++- .github/skills/mattpocock-handoff/SKILL.md | 16 + .github/skills/mattpocock-research/SKILL.md | 12 + .github/skills/openai-docx/SKILL.md | 2 +- .github/skills/openai-gh-fix-ci/SKILL.md | 2 +- .github/skills/openai-pdf/SKILL.md | 2 +- .../spec-document-reviewer-prompt.md | 2 +- .../superpowers-executing-plans/SKILL.md | 10 +- .../SKILL.md | 18 +- .../superpowers-systematic-debugging/SKILL.md | 6 +- .../superpowers-using-superpowers/SKILL.md | 4 +- .../skills/superpowers-writing-plans/SKILL.md | 8 +- .../references/CI_CD.md | 2 +- .github/skills/vercel-find-skills/SKILL.md | 142 +++++++ 24 files changed, 1799 insertions(+), 45 deletions(-) create mode 100644 .github/skills/addyosmani-code-review-and-quality/SKILL.md create mode 100644 .github/skills/awesome-copilot-security-review/SKILL.md create mode 100644 .github/skills/awesome-copilot-security-review/references/language-patterns.md create mode 100644 .github/skills/awesome-copilot-security-review/references/report-format.md create mode 100644 .github/skills/awesome-copilot-security-review/references/secret-patterns.md create mode 100644 .github/skills/awesome-copilot-security-review/references/vuln-categories.md create mode 100644 .github/skills/awesome-copilot-security-review/references/vulnerable-packages.md create mode 100644 .github/skills/mattpocock-handoff/SKILL.md create mode 100644 .github/skills/mattpocock-research/SKILL.md create mode 100644 .github/skills/vercel-find-skills/SKILL.md diff --git a/.github/INVENTORY.md b/.github/INVENTORY.md index 6d9ca5c6..c9a99ea1 100644 --- a/.github/INVENTORY.md +++ b/.github/INVENTORY.md @@ -32,6 +32,7 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi ## Skills +- `.github/skills/addyosmani-code-review-and-quality/SKILL.md` - `.github/skills/agent-os-discover-standards/SKILL.md` - `.github/skills/agent-os-index-standards/SKILL.md` - `.github/skills/agent-os-inject-standards/SKILL.md` @@ -53,6 +54,7 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/awesome-copilot-codeql/SKILL.md` - `.github/skills/awesome-copilot-dependabot/SKILL.md` - `.github/skills/awesome-copilot-secret-scanning/SKILL.md` +- `.github/skills/awesome-copilot-security-review/SKILL.md` - `.github/skills/grill-me/SKILL.md` - `.github/skills/internal-agent-creator/SKILL.md` - `.github/skills/internal-agent-support-lane-change-engine/SKILL.md` @@ -123,6 +125,8 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/local-agent-sync-global-copilot-configs-into-repo/SKILL.md` - `.github/skills/local-agent-sync-install-ai-resources/SKILL.md` - `.github/skills/local-copilot-log-analyzer/SKILL.md` +- `.github/skills/mattpocock-handoff/SKILL.md` +- `.github/skills/mattpocock-research/SKILL.md` - `.github/skills/openai-docx/SKILL.md` - `.github/skills/openai-gh-address-comments/SKILL.md` - `.github/skills/openai-gh-fix-ci/SKILL.md` @@ -145,6 +149,7 @@ This file is the exact path inventory for the live GitHub Copilot catalog in thi - `.github/skills/superpowers-writing-plans/SKILL.md` - `.github/skills/terraform-terraform-search-import/SKILL.md` - `.github/skills/terraform-terraform-test/SKILL.md` +- `.github/skills/vercel-find-skills/SKILL.md` ### Support-only imported office skills diff --git a/.github/skills/addyosmani-code-review-and-quality/SKILL.md b/.github/skills/addyosmani-code-review-and-quality/SKILL.md new file mode 100644 index 00000000..ad789bf8 --- /dev/null +++ b/.github/skills/addyosmani-code-review-and-quality/SKILL.md @@ -0,0 +1,381 @@ +--- +name: addyosmani-code-review-and-quality +description: Conducts multi-axis code review. Use before merging any change. Use when reviewing code written by yourself, another agent, or a human. Use when you need to assess code quality across multiple dimensions before it enters the main branch. +--- + +# Code Review and Quality + +## Overview + +Multi-dimensional code review with quality gates. Every change gets reviewed before merge — no exceptions. Review covers five axes: correctness, readability, architecture, security, and performance. + +**The approval standard:** Approve a change when it definitely improves overall code health, even if it isn't perfect. Perfect code doesn't exist — the goal is continuous improvement. Don't block a change because it isn't exactly how you would have written it. If it improves the codebase and follows the project's conventions, approve it. + +## When to Use + +- Before merging any PR or change +- After completing a feature implementation +- When another agent or model produced code you need to evaluate +- When refactoring existing code +- After any bug fix (review both the fix and the regression test) + +## The Five-Axis Review + +Every review evaluates code across these dimensions: + +### 1. Correctness + +Does the code do what it claims to do? + +- Does it match the spec or task requirements? +- Are edge cases handled (null, empty, boundary values)? +- Are error paths handled (not just the happy path)? +- Does it pass all tests? Are the tests actually testing the right things? +- Are there off-by-one errors, race conditions, or state inconsistencies? + +### 2. Readability & Simplicity + +Can another engineer (or agent) understand this code without the author explaining it? + +- Are names descriptive and consistent with project conventions? (No `temp`, `data`, `result` without context) +- Is the control flow straightforward (avoid nested ternaries, deep callbacks)? +- Is the code organized logically (related code grouped, clear module boundaries)? +- Are there any "clever" tricks that should be simplified? +- **Could this be done in fewer lines?** (1000 lines where 100 suffice is a failure) +- **Are abstractions earning their complexity?** (Don't generalize until the third use case) +- Would comments help clarify non-obvious intent? (But don't comment obvious code.) +- Are there dead code artifacts: no-op variables (`_unused`), backwards-compat shims, or `// removed` comments? +- **Is a new conditional bolted onto an unrelated flow?** That's a design smell, not a nit — push the logic into its own helper, state, or policy instead of tangling an existing path. +- **Do repeated conditionals on the same shape appear?** They signal a missing model or dispatcher. A "temporary" branch is usually permanent debt. + +### 3. Architecture + +Does the change fit the system's design? + +- Does it follow existing patterns or introduce a new one? If new, is it justified? +- Does it maintain clean module boundaries? +- Is there code duplication that should be shared? +- Are dependencies flowing in the right direction (no circular dependencies)? +- Is the abstraction level appropriate (not over-engineered, not too coupled)? +- **Does this refactor reduce complexity or just relocate it?** Count the concepts a reader must hold to follow the change. If a "cleaner" version leaves that count unchanged, it isn't cleaner — prefer the restructuring that makes whole branches, modes, or layers disappear over one that re-centralizes the same logic. Prefer deleting an abstraction to polishing it. +- **Is feature-specific logic leaking into a shared or general-purpose module?** Keep logic in its owning layer, reuse the existing canonical helper instead of a near-duplicate, and don't normalize architectural drift. +- **Are type boundaries explicit?** Question gratuitous `any`/`unknown`/optional/casts and silent fallbacks that paper over an unclear invariant — making the boundary explicit often makes the surrounding control flow simpler. + +### 4. Security + +For detailed security guidance, see `security-and-hardening`. Does the change introduce vulnerabilities? + +- Is user input validated and sanitized? +- Are secrets kept out of code, logs, and version control? +- Is authentication/authorization checked where needed? +- Are SQL queries parameterized (no string concatenation)? +- Are outputs encoded to prevent XSS? +- Are dependencies from trusted sources with no known vulnerabilities? +- Is data from external sources (APIs, logs, user content, config files) treated as untrusted? +- Are external data flows validated at system boundaries before use in logic or rendering? + +### 5. Performance + +For detailed profiling and optimization, see `performance-optimization`. Does the change introduce performance problems? + +- Any N+1 query patterns? +- Any unbounded loops or unconstrained data fetching? +- Any synchronous operations that should be async? +- Any unnecessary re-renders in UI components? +- Any missing pagination on list endpoints? +- Any large objects created in hot paths? + +## Structural Remedies + +When you flag a structural problem, propose the move — not just the problem. A review that only says "this is complex" leaves the author guessing. Reach for a named restructuring: + +- **Replace a chain of conditionals** with a typed model or an explicit dispatcher. +- **Collapse duplicate branches** into a single clearer flow. +- **Separate orchestration from business logic** so each reads on its own. +- **Move feature-specific logic** out of a shared module into the package that owns the concept. +- **Reuse the canonical helper** instead of a bespoke near-duplicate. +- **Make a type boundary explicit** so downstream branching disappears. +- **Delete a pass-through wrapper** that adds indirection without clarifying the API. +- **Extract a helper, or split a large file** into focused modules. + +Prefer the remedy that removes moving pieces over one that spreads the same complexity around. + +## Change Sizing + +Small, focused changes are easier to review, faster to merge, and safer to deploy. Target these sizes: + +``` +~100 lines changed → Good. Reviewable in one sitting. +~300 lines changed → Acceptable if it's a single logical change. +~1000 lines changed → Too large. Split it. +``` + +**Watch file size, not just diff size.** A small diff can still push a file past a healthy boundary — around 1000 *total* lines in a single file (distinct from the ~1000 *changed*-lines threshold above) is a common inspection signal, not a hard cap. When a change materially grows an already-large file, ask whether to extract helpers, subcomponents, or modules *first*, before piling more on. Decompose, then add. + +**What counts as "one change":** A single self-contained modification that addresses one thing, includes related tests, and keeps the system functional after submission. One part of a feature — not the whole feature. + +**Splitting strategies when a change is too large:** + +| Strategy | How | When | +|----------|-----|------| +| **Stack** | Submit a small change, start the next one based on it | Sequential dependencies | +| **By file group** | Separate changes for groups needing different reviewers | Cross-cutting concerns | +| **Horizontal** | Create shared code/stubs first, then consumers | Layered architecture | +| **Vertical** | Break into smaller full-stack slices of the feature | Feature work | + +**When large changes are acceptable:** Complete file deletions and automated refactoring where the reviewer only needs to verify intent, not every line. + +**Separate refactoring from feature work.** A change that refactors existing code and adds new behavior is two changes — submit them separately. Small cleanups (variable renaming) can be included at reviewer discretion. + +## Change Descriptions + +Every change needs a description that stands alone in version control history. + +**First line:** Short, imperative, standalone. "Delete the FizzBuzz RPC" not "Deleting the FizzBuzz RPC." Must be informative enough that someone searching history can understand the change without reading the diff. + +**Body:** What is changing and why. Include context, decisions, and reasoning not visible in the code itself. Link to bug numbers, benchmark results, or design docs where relevant. Acknowledge approach shortcomings when they exist. + +**Anti-patterns:** "Fix bug," "Fix build," "Add patch," "Moving code from A to B," "Phase 1," "Add convenience functions." + +## Review Process + +### Step 1: Understand the Context + +Before looking at code, understand the intent: + +``` +- What is this change trying to accomplish? +- What spec or task does it implement? +- What is the expected behavior change? +``` + +### Step 2: Review the Tests First + +Tests reveal intent and coverage: + +``` +- Do tests exist for the change? +- Do they test behavior (not implementation details)? +- Are edge cases covered? +- Do tests have descriptive names? +- Would the tests catch a regression if the code changed? +``` + +### Step 3: Review the Implementation + +Walk through the code with the five axes in mind: + +``` +For each file changed: +1. Correctness: Does this code do what the test says it should? +2. Readability: Can I understand this without help? +3. Architecture: Does this fit the system? +4. Security: Any vulnerabilities? +5. Performance: Any bottlenecks? +``` + +### Step 4: Categorize Findings + +Label every comment with its severity so the author knows what's required vs optional: + +| Prefix | Meaning | Author Action | +|--------|---------|---------------| +| *(no prefix)* | Required change | Must address before merge | +| **Critical:** | Blocks merge | Security vulnerability, data loss, broken functionality | +| **Nit:** | Minor, optional | Author may ignore — formatting, style preferences | +| **Optional:** / **Consider:** | Suggestion | Worth considering but not required | +| **FYI** | Informational only | No action needed — context for future reference | + +This prevents authors from treating all feedback as mandatory and wasting time on optional suggestions. + +**Lead with what matters.** Order findings by leverage: correctness and security first, then structural regressions and missed simplifications, then everything else. Don't bury a real issue under cosmetic nits — a few high-conviction comments beat a long list. If you have one structural problem and ten nits, the structural problem *is* the review. + +### Step 5: Verify the Verification + +Check the author's verification story: + +``` +- What tests were run? +- Did the build pass? +- Was the change tested manually? +- Are there screenshots for UI changes? +- Is there a before/after comparison? +``` + +## Multi-Model Review Pattern + +Use different models for different review perspectives: + +``` +Model A writes the code + │ + ▼ +Model B reviews for correctness and architecture + │ + ▼ +Model A addresses the feedback + │ + ▼ +Human makes the final call +``` + +This catches issues that a single model might miss — different models have different blind spots. + +**Example prompt for a review agent:** +``` +Review this code change for correctness, security, and adherence to +our project conventions. The spec says [X]. The change should [Y]. +Flag any issues as Critical, Required, Optional, or Nit. +``` + +## Dead Code Hygiene + +After any refactoring or implementation change, check for orphaned code: + +1. Identify code that is now unreachable or unused +2. List it explicitly +3. **Ask before deleting:** "Should I remove these now-unused elements: [list]?" + +Don't leave dead code lying around — it confuses future readers and agents. But don't silently delete things you're not sure about. When in doubt, ask. + +``` +DEAD CODE IDENTIFIED: +- formatLegacyDate() in src/utils/date.ts — replaced by formatDate() +- OldTaskCard component in src/components/ — replaced by TaskCard +- LEGACY_API_URL constant in src/config.ts — no remaining references +→ Safe to remove these? +``` + +## Review Speed + +Slow reviews block entire teams. The cost of context-switching to review is less than the waiting cost imposed on others. + +- **Respond within one business day** — this is the maximum, not the target +- **Ideal cadence:** Respond shortly after a review request arrives, unless deep in focused coding. A typical change should complete multiple review rounds in a single day +- **Prioritize fast individual responses** over quick final approval. Quick feedback reduces frustration even if multiple rounds are needed +- **Large changes:** Ask the author to split them rather than reviewing one massive changeset + +## Handling Disagreements + +When resolving review disputes, apply this hierarchy: + +1. **Technical facts and data** override opinions and preferences +2. **Style guides** are the absolute authority on style matters +3. **Software design** must be evaluated on engineering principles, not personal preference +4. **Codebase consistency** is acceptable if it doesn't degrade overall health + +**Don't accept "I'll clean it up later."** Experience shows deferred cleanup rarely happens. Require cleanup before submission unless it's a genuine emergency. If surrounding issues can't be addressed in this change, require filing a bug with self-assignment. + +## Honesty in Review + +When reviewing code — whether written by you, another agent, or a human: + +- **Don't rubber-stamp.** "LGTM" without evidence of review helps no one. +- **Don't soften real issues.** "This might be a minor concern" when it's a bug that will hit production is dishonest. +- **Quantify problems when possible.** "This N+1 query will add ~50ms per item in the list" is better than "this could be slow." +- **Push back on approaches with clear problems.** Sycophancy is a failure mode in reviews. If the implementation has issues, say so directly and propose alternatives. +- **Accept override gracefully.** If the author has full context and disagrees, defer to their judgment. Comment on code, not people — reframe personal critiques to focus on the code itself. + +## Dependency Discipline + +Part of code review is dependency review: + +**Before adding any dependency:** +1. Does the existing stack solve this? (Often it does.) +2. How large is the dependency? (Check bundle impact.) +3. Is it actively maintained? (Check last commit, open issues.) +4. Does it have known vulnerabilities? (`npm audit`) +5. What's the license? (Must be compatible with the project.) + +**Rule:** Prefer standard library and existing utilities over new dependencies. Every dependency is a liability. + +## The Review Checklist + +```markdown +## Review: [PR/Change title] + +### Context +- [ ] I understand what this change does and why + +### Correctness +- [ ] Change matches spec/task requirements +- [ ] Edge cases handled +- [ ] Error paths handled +- [ ] Tests cover the change adequately + +### Readability +- [ ] Names are clear and consistent +- [ ] Logic is straightforward +- [ ] No unnecessary complexity + +### Architecture +- [ ] Follows existing patterns +- [ ] No unnecessary coupling or dependencies +- [ ] Appropriate abstraction level +- [ ] Refactors reduce complexity rather than relocate it +- [ ] No feature logic in shared modules; file stays within a healthy size + +### Security +- [ ] No secrets in code +- [ ] Input validated at boundaries +- [ ] No injection vulnerabilities +- [ ] Auth checks in place +- [ ] External data sources treated as untrusted + +### Performance +- [ ] No N+1 patterns +- [ ] No unbounded operations +- [ ] Pagination on list endpoints + +### Verification +- [ ] Tests pass +- [ ] Build succeeds +- [ ] Manual verification done (if applicable) + +### Verdict +- [ ] **Approve** — Ready to merge +- [ ] **Request changes** — Issues must be addressed +``` +## See Also + +- For detailed security review guidance, see `references/security-checklist.md` +- For performance review checks, see `references/performance-checklist.md` + +## Common Rationalizations + +| Rationalization | Reality | +|---|---| +| "It works, that's good enough" | Working code that's unreadable, insecure, or architecturally wrong creates debt that compounds. | +| "I wrote it, so I know it's correct" | Authors are blind to their own assumptions. Every change benefits from another set of eyes. | +| "We'll clean it up later" | Later never comes. The review is the quality gate — use it. Require cleanup before merge, not after. | +| "AI-generated code is probably fine" | AI code needs more scrutiny, not less. It's confident and plausible, even when wrong. | +| "The tests pass, so it's good" | Tests are necessary but not sufficient. They don't catch architecture problems, security issues, or readability concerns. | +| "The refactor makes it cleaner" | Relocating complexity isn't reducing it. If the reader still holds the same number of concepts, the structure didn't improve — look for the version where branches disappear. | +| "It's only a small addition to this file" | Small diffs still push files past a healthy size and bolt branches onto unrelated flows. Judge the resulting structure, not the diff size. | + +## Red Flags + +- PRs merged without any review +- Review that only checks if tests pass (ignoring other axes) +- "LGTM" without evidence of actual review +- Security-sensitive changes without security-focused review +- Large PRs that are "too big to review properly" (split them) +- No regression tests with bug fix PRs +- Review comments without severity labels — makes it unclear what's required vs optional +- Accepting "I'll fix it later" — it never happens +- A refactor that moves code around without reducing the number of concepts a reader must hold +- A change that grows an already-large file instead of decomposing it +- New conditionals scattered into unrelated code paths (a missing abstraction) +- A bespoke helper that duplicates an existing canonical one, or feature logic placed in a shared module + +## Verification + +After review is complete: + +- [ ] All Critical issues are resolved +- [ ] All Required (no-prefix) changes are resolved or explicitly deferred with justification +- [ ] Tests pass +- [ ] Build succeeds +- [ ] The verification story is documented (what changed, how it was verified) + +**Presumptive blockers:** surface and propose the simpler design for each of these; escalate to Required only when the change actively makes structure worse: a refactor that relocates complexity instead of reducing it; a change that pushes a file past the size boundary with no decomposition; feature logic added to a shared module; a near-duplicate of an existing canonical helper; a silent fallback that hides an unclear invariant. diff --git a/.github/skills/awesome-copilot-codeql/references/workflow-configuration.md b/.github/skills/awesome-copilot-codeql/references/workflow-configuration.md index 6700a00f..5984cb90 100644 --- a/.github/skills/awesome-copilot-codeql/references/workflow-configuration.md +++ b/.github/skills/awesome-copilot-codeql/references/workflow-configuration.md @@ -263,7 +263,7 @@ The `category` value appears as `.automationDetails.id` in the SARIF output Create `.github/codeql/codeql-config.yml` for advanced path and query configuration: ```yaml -name: "CodeQL Configuration" +name: awesome-copilot-codeql # Directories to scan paths: diff --git a/.github/skills/awesome-copilot-security-review/SKILL.md b/.github/skills/awesome-copilot-security-review/SKILL.md new file mode 100644 index 00000000..9335156c --- /dev/null +++ b/.github/skills/awesome-copilot-security-review/SKILL.md @@ -0,0 +1,168 @@ +--- +name: awesome-copilot-security-review +description: 'AI-powered codebase security scanner that reasons about code like a security researcher — tracing data flows, understanding component interactions, and catching vulnerabilities that pattern-matching tools miss. Use this skill when asked to scan code for security vulnerabilities, find bugs, check for SQL injection, XSS, command injection, exposed API keys, hardcoded secrets, insecure dependencies, access control issues, or any request like "is my code secure?", "review for security issues", "audit this codebase", or "check for vulnerabilities". Covers injection flaws, authentication and access control bugs, secrets exposure, weak cryptography, insecure dependencies, and business logic issues across JavaScript, TypeScript, Python, Java, PHP, Go, Ruby, and Rust.' +--- + +# Security Review + +An AI-powered security scanner that reasons about your codebase the way a human security +researcher would — tracing data flows, understanding component interactions, and catching +vulnerabilities that pattern-matching tools miss. + +## When to Use This Skill + +Use this skill when the request involves: + +- Scanning a codebase or file for security vulnerabilities +- Running a security review or vulnerability check +- Checking for SQL injection, XSS, command injection, or other injection flaws +- Finding exposed API keys, hardcoded secrets, or credentials in code +- Auditing dependencies for known CVEs +- Reviewing authentication, authorization, or access control logic +- Detecting insecure cryptography or weak randomness +- Performing a data flow analysis to trace user input to dangerous sinks +- Any request phrasing like "is my code secure?", "scan this file", or "check my repo for vulnerabilities" +- Running `/security-review` or `/security-review ` + +## How This Skill Works + +Unlike traditional static analysis tools that match patterns, this skill: +1. **Reads code like a security researcher** — understanding context, intent, and data flow +2. **Traces across files** — following how user input moves through your application +3. **Self-verifies findings** — re-examines each result to filter false positives +4. **Assigns severity ratings** — CRITICAL / HIGH / MEDIUM / LOW / INFO +5. **Proposes targeted patches** — every finding includes a concrete fix +6. **Requires human approval** — nothing is auto-applied; you always review first + +## Execution Workflow + +Follow these steps **in order** every time: + +### Step 1 — Scope Resolution +Determine what to scan: +- If a path was provided (`/security-review src/auth/`), scan only that scope +- If no path given, scan the **entire project** starting from the root +- Identify the language(s) and framework(s) in use (check package.json, requirements.txt, + go.mod, Cargo.toml, pom.xml, Gemfile, composer.json, etc.) +- Read `references/language-patterns.md` to load language-specific vulnerability patterns + +### Step 2 — Dependency Audit +Before scanning source code, audit dependencies first (fast wins): +- **Node.js**: Check `package.json` + `package-lock.json` for known vulnerable packages +- **Python**: Check `requirements.txt` / `pyproject.toml` / `Pipfile` +- **Java**: Check `pom.xml` / `build.gradle` +- **Ruby**: Check `Gemfile.lock` +- **Rust**: Check `Cargo.toml` +- **Go**: Check `go.sum` +- Flag packages with known CVEs, deprecated crypto libs, or suspiciously old pinned versions +- Read `references/vulnerable-packages.md` for a curated watchlist + +### Step 3 — Secrets & Exposure Scan +Scan ALL files (including config, env, CI/CD, Dockerfiles, IaC) for: +- Hardcoded API keys, tokens, passwords, private keys +- `.env` files accidentally committed +- Secrets in comments or debug logs +- Cloud credentials (AWS, GCP, Azure, Stripe, Twilio, etc.) +- Database connection strings with credentials embedded +- Read `references/secret-patterns.md` for regex patterns and entropy heuristics to apply + +### Step 4 — Vulnerability Deep Scan +This is the core scan. Reason about the code — don't just pattern-match. +Read `references/vuln-categories.md` for full details on each category. + +**Injection Flaws** +- SQL Injection: raw queries with string interpolation, ORM misuse, second-order SQLi +- XSS: unescaped output, dangerouslySetInnerHTML, innerHTML, template injection +- Command Injection: exec/spawn/system with user input +- LDAP, XPath, Header, Log injection + +**Authentication & Access Control** +- Missing authentication on sensitive endpoints +- Broken object-level authorization (BOLA/IDOR) +- JWT weaknesses (alg:none, weak secrets, no expiry validation) +- Session fixation, missing CSRF protection +- Privilege escalation paths +- Mass assignment / parameter pollution + +**Data Handling** +- Sensitive data in logs, error messages, or API responses +- Missing encryption at rest or in transit +- Insecure deserialization +- Path traversal / directory traversal +- XXE (XML External Entity) processing +- SSRF (Server-Side Request Forgery) + +**Cryptography** +- Use of MD5, SHA1, DES for security purposes +- Hardcoded IVs or salts +- Weak random number generation (Math.random() for tokens) +- Missing TLS certificate validation + +**Business Logic** +- Race conditions (TOCTOU) +- Integer overflow in financial calculations +- Missing rate limiting on sensitive endpoints +- Predictable resource identifiers + +### Step 5 — Cross-File Data Flow Analysis +After the per-file scan, perform a **holistic review**: +- Trace user-controlled input from entry points (HTTP params, headers, body, file uploads) + all the way to sinks (DB queries, exec calls, HTML output, file writes) +- Identify vulnerabilities that only appear when looking at multiple files together +- Check for insecure trust boundaries between services or modules + +### Step 6 — Self-Verification Pass +For EACH finding: +1. Re-read the relevant code with fresh eyes +2. Ask: "Is this actually exploitable, or is there sanitization I missed?" +3. Check if a framework or middleware already handles this upstream +4. Downgrade or discard findings that aren't genuine vulnerabilities +5. Assign final severity: CRITICAL / HIGH / MEDIUM / LOW / INFO + +### Step 7 — Generate Security Report +Output the full report in the format defined in `references/report-format.md`. + +### Step 8 — Propose Patches +For every CRITICAL and HIGH finding, generate a concrete patch: +- Show the vulnerable code (before) +- Show the fixed code (after) +- Explain what changed and why +- Preserve the original code style, variable names, and structure +- Add a comment explaining the fix inline + +Explicitly state: **"Review each patch before applying. Nothing has been changed yet."** + +## Severity Guide + +| Severity | Meaning | Example | +|----------|---------|---------| +| 🔴 CRITICAL | Immediate exploitation risk, data breach likely | SQLi, RCE, auth bypass | +| 🟠 HIGH | Serious vulnerability, exploit path exists | XSS, IDOR, hardcoded secrets | +| 🟡 MEDIUM | Exploitable with conditions or chaining | CSRF, open redirect, weak crypto | +| 🔵 LOW | Best practice violation, low direct risk | Verbose errors, missing headers | +| ⚪ INFO | Observation worth noting, not a vulnerability | Outdated dependency (no CVE) | + +## Output Rules + +- **Always** produce a findings summary table first (counts by severity) +- **Never** auto-apply any patch — present patches for human review only +- **Always** include a confidence rating per finding (High / Medium / Low) +- **Group findings** by category, not by file +- **Be specific** — include file path, line number, and the exact vulnerable code snippet +- **Explain the risk** in plain English — what could an attacker do with this? +- If the codebase is clean, say so clearly: "No vulnerabilities found" with what was scanned + +## Reference Files + +For detailed detection guidance, load the following reference files as needed: + +- `references/vuln-categories.md` — Deep reference for every vulnerability category with detection signals, safe patterns, and escalation checkers + - Search patterns: `SQL injection`, `XSS`, `command injection`, `SSRF`, `BOLA`, `IDOR`, `JWT`, `CSRF`, `secrets`, `cryptography`, `race condition`, `path traversal` +- `references/secret-patterns.md` — Regex patterns, entropy-based detection, and CI/CD secret risks + - Search patterns: `API key`, `token`, `private key`, `connection string`, `entropy`, `.env`, `GitHub Actions`, `Docker`, `Terraform` +- `references/language-patterns.md` — Framework-specific vulnerability patterns for JavaScript, Python, Java, PHP, Go, Ruby, and Rust + - Search patterns: `Express`, `React`, `Next.js`, `Django`, `Flask`, `FastAPI`, `Spring Boot`, `PHP`, `Go`, `Rails`, `Rust` +- `references/vulnerable-packages.md` — Curated CVE watchlist for npm, pip, Maven, Rubygems, Cargo, and Go modules + - Search patterns: `lodash`, `axios`, `jsonwebtoken`, `Pillow`, `log4j`, `nokogiri`, `CVE` +- `references/report-format.md` — Structured output template for security reports with finding cards, dependency audit, secrets scan, and patch proposal formatting + - Search patterns: `report`, `format`, `template`, `finding`, `patch`, `summary`, `confidence` diff --git a/.github/skills/awesome-copilot-security-review/references/language-patterns.md b/.github/skills/awesome-copilot-security-review/references/language-patterns.md new file mode 100644 index 00000000..d6af534a --- /dev/null +++ b/.github/skills/awesome-copilot-security-review/references/language-patterns.md @@ -0,0 +1,221 @@ +# Language-Specific Vulnerability Patterns + +Load the relevant section during Step 1 (Scope Resolution) after identifying languages. + +--- + +## JavaScript / TypeScript (Node.js, React, Next.js, Express) + +### Critical APIs/calls to flag +```js +eval() // arbitrary code execution +Function('return ...') // same as eval +child_process.exec() // command injection if user input reaches it +fs.readFile // path traversal if user controls path +fs.writeFile // path traversal if user controls path +``` + +### Express.js specific +```js +// Missing helmet (security headers) +const app = express() +// Should have: app.use(helmet()) + +// Body size limits missing (DoS) +app.use(express.json()) +// Should have: app.use(express.json({ limit: '10kb' })) + +// CORS misconfiguration +app.use(cors({ origin: '*' })) // too permissive +app.use(cors({ origin: req.headers.origin })) // reflects any origin + +// Trust proxy without validation +app.set('trust proxy', true) // only safe behind known proxy +``` + +### React specific +```jsx +
// XSS +link // javascript: URL injection +``` + +### Next.js specific +```js +// Server Actions without auth +export async function deleteUser(id) { // missing: auth check + await db.users.delete(id) +} + +// API Routes missing method validation +export default function handler(req, res) { + // Should check: if (req.method !== 'POST') return res.status(405) + doSensitiveAction() +} +``` + +--- + +## Python (Django, Flask, FastAPI) + +### Django specific +```python +# Raw SQL +User.objects.raw(f"SELECT * FROM users WHERE name = '{name}'") # SQLi + +# Missing CSRF +@csrf_exempt # Only OK for APIs with token auth + +# Debug mode in production +DEBUG = True # in settings.py — exposes stack traces + +# SECRET_KEY +SECRET_KEY = 'django-insecure-...' # must be changed for production + +# ALLOWED_HOSTS +ALLOWED_HOSTS = ['*'] # too permissive +``` + +### Flask specific +```python +# Debug mode +app.run(debug=True) # never in production + +# Secret key +app.secret_key = 'dev' # weak + +# eval/exec with user input +eval(request.args.get('expr')) + +# render_template_string with user input (SSTI) +render_template_string(f"Hello {name}") # Server-Side Template Injection +``` + +### FastAPI specific +```python +# Missing auth dependency +@app.delete("/users/{user_id}") # No Depends(get_current_user) +async def delete_user(user_id: int): + ... + +# Arbitrary file read +@app.get("/files/{filename}") +async def read_file(filename: str): + return FileResponse(f"uploads/{filename}") # path traversal +``` + +--- + +## Java (Spring Boot) + +### Spring Boot specific +```java +// SQL Injection +String query = "SELECT * FROM users WHERE name = '" + name + "'"; +jdbcTemplate.query(query, ...); + +// XXE +DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); +// Missing: dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) + +// Deserialization +ObjectInputStream ois = new ObjectInputStream(inputStream); +Object obj = ois.readObject(); // only safe with allowlist + +// Spring Security — permitAll on sensitive endpoint +.antMatchers("/admin/**").permitAll() + +// Actuator endpoints exposed +management.endpoints.web.exposure.include=* # in application.properties +``` + +--- + +## PHP + +```php +// Direct user input in queries +$result = mysql_query("SELECT * FROM users WHERE id = " . $_GET['id']); + +// File inclusion +include($_GET['page'] . ".php"); // local/remote file inclusion + +// eval +eval($_POST['code']); + +// extract() with user input +extract($_POST); // overwrites any variable + +// Loose comparison +if ($password == "admin") {} // use === instead + +// Unserialize +unserialize($_COOKIE['data']); // remote code execution +``` + +--- + +## Go + +```go +// Command injection +exec.Command("sh", "-c", userInput) + +// SQL injection +db.Query("SELECT * FROM users WHERE name = '" + name + "'") + +// Path traversal +filePath := filepath.Join("/uploads/", userInput) // sanitize userInput first + +// Insecure TLS +http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + +// Goroutine leak / missing context cancellation +go func() { + // No done channel or context + for { ... } +}() +``` + +--- + +## Ruby on Rails + +```ruby +# SQL injection (safe alternatives use placeholders) +User.where("name = '#{params[:name]}'") # VULNERABLE +User.where("name = ?", params[:name]) # SAFE + +# Mass assignment without strong params +@user.update(params[:user]) # should be params.require(:user).permit(...) + +# eval / send with user input +eval(params[:code]) +send(params[:method]) # arbitrary method call + +# Redirect to user-supplied URL (open redirect) +redirect_to params[:url] + +# YAML.load (allows arbitrary object creation) +YAML.load(user_input) # use YAML.safe_load instead +``` + +--- + +## Rust + +```rust +// Unsafe blocks — flag for manual review +unsafe { + // Reason for unsafety should be documented +} + +// Integer overflow (debug builds panic, release silently wraps) +let result = a + b; // use checked_add/saturating_add for financial math + +// Unwrap/expect in production code (panics on None/Err) +let value = option.unwrap(); // prefer ? or match + +// Deserializing arbitrary types +serde_json::from_str::(&user_input) // generally safe +// But: bincode::deserialize from untrusted input — can be exploited +``` diff --git a/.github/skills/awesome-copilot-security-review/references/report-format.md b/.github/skills/awesome-copilot-security-review/references/report-format.md new file mode 100644 index 00000000..55fe5717 --- /dev/null +++ b/.github/skills/awesome-copilot-security-review/references/report-format.md @@ -0,0 +1,194 @@ +# Security Report Format + +Use this template for all `/security-review` output. Generated during Step 7. + +--- + +## Report Structure + +### Header +``` +╔══════════════════════════════════════════════════════════╗ +║ 🔐 SECURITY REVIEW REPORT ║ +║ Generated by: /security-review skill ║ +╚══════════════════════════════════════════════════════════╝ + +Project: +Scan Date: +Scope: +Languages Detected: +Frameworks Detected: +``` + +--- + +### Executive Summary Table + +Always show this first — at a glance overview: + +``` +┌────────────────────────────────────────────────┐ +│ FINDINGS SUMMARY │ +├──────────────┬──────────────────────────────── ┤ +│ 🔴 CRITICAL │ findings │ +│ 🟠 HIGH │ findings │ +│ 🟡 MEDIUM │ findings │ +│ 🔵 LOW │ findings │ +│ ⚪ INFO │ findings │ +├──────────────┼─────────────────────────────────┤ +│ TOTAL │ findings │ +└──────────────┴─────────────────────────────────┘ + +Dependency Audit: vulnerable packages found +Secrets Scan: exposed credentials found +``` + +--- + +### Findings (Grouped by Category) + +For EACH finding, use this card format: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +[SEVERITY EMOJI] [SEVERITY] — [VULNERABILITY TYPE] +Confidence: HIGH / MEDIUM / LOW +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📍 Location: src/routes/users.js, Line 47 + +🔍 Vulnerable Code: + const query = `SELECT * FROM users WHERE id = ${req.params.id}`; + db.execute(query); + +⚠️ Risk: + An attacker can manipulate the `id` parameter to execute arbitrary + SQL commands, potentially dumping the entire database, bypassing + authentication, or deleting data. + + Example attack: GET /users/1 OR 1=1-- + +✅ Recommended Fix: + Use parameterized queries: + + const query = 'SELECT * FROM users WHERE id = ?'; + db.execute(query, [req.params.id]); + +📚 Reference: OWASP A03:2021 – Injection +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +``` + +--- + +### Dependency Audit Section + +``` +📦 DEPENDENCY AUDIT +══════════════════ + +🟠 HIGH — lodash@4.17.20 (package.json) + CVE-2021-23337: Prototype pollution via zipObjectDeep() + Fix: npm install lodash@4.17.21 + +🟡 MEDIUM — axios@0.27.2 (package.json) + CVE-2023-45857: CSRF via withCredentials + Fix: npm install axios@1.6.0 + +⚪ INFO — express@4.18.2 + No known CVEs. Current version is 4.19.2 — consider updating. +``` + +--- + +### Secrets Scan Section + +``` +🔑 SECRETS & EXPOSURE SCAN +═══════════════════════════ + +🔴 CRITICAL — Hardcoded API Key + File: src/config/database.js, Line 12 + + Found: STRIPE_SECRET_KEY = "sk_live_FAKE_KEY_..." + + Action Required: + 1. Rotate this key IMMEDIATELY at https://dashboard.stripe.com + 2. Remove from source code + 3. Add to .env file and load via process.env.STRIPE_SECRET_KEY + 4. Add .env to .gitignore + 5. Audit git history — key may be in previous commits: + git log --all -p | grep "sk_live_" + Use git-filter-repo or BFG to purge from history if found. +``` + +--- + +### Patch Proposals Section + +Only include for CRITICAL and HIGH findings: + +```` +🛠️ PATCH PROPOSALS +══════════════════ +⚠️ REVIEW EACH PATCH BEFORE APPLYING — Nothing has been changed yet. + +───────────────────────────────────────────── +Patch 1/3: SQL Injection in src/routes/users.js +───────────────────────────────────────────── + +BEFORE (vulnerable): +```js +// Line 47 +const query = `SELECT * FROM users WHERE id = ${req.params.id}`; +db.execute(query); +``` + +AFTER (fixed): +```js +// Line 47 — Fixed: Use parameterized query to prevent SQL injection +const query = 'SELECT * FROM users WHERE id = ?'; +db.execute(query, [req.params.id]); +``` + +Apply this patch? (Review first — AI-generated patches may need adjustment) +───────────────────────────────────────────── +```` + +--- + +### Footer + +``` +══════════════════════════════════════════════════════════ + +📋 SCAN COVERAGE + Files scanned: + Lines analyzed: + Scan duration: